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
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.
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() {
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.
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
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
98 if (!this.__storage
) {
100 var contractID
= "@mozilla.org/login-manager/storage/mozStorage;1";
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
);
109 this.log("No alternate nsILoginManagerStorage registered");
112 this.__storage
= Cc
[contractID
].
113 createInstance(Ci
.nsILoginManagerStorage
);
115 this.__storage
.init();
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
135 * Initialize the Login Manager. Automatically called when service
138 * Note: Service created in /browser/base/content/browser.js,
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
);
183 * Internal function for logging debug messages to the Error Console window
185 log : function (message
) {
188 dump("Login Manager: " + message
+ "\n");
189 this._logService
.logStringMessage("Login Manager: " + message
);
193 /* ---------- Utility objects ---------- */
199 * Internal utility object, implements the nsIObserver interface.
200 * Used to receive notification for: form submission, preference changes.
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).
218 this._pwmgr
._onFormSubmit(formElement
);
220 this._pwmgr
.log("Caught error in onFormSubmit: " + e
);
223 return true; // Always return true, or form submit will be canceled.
227 observe : function (subject
, topic
, data
) {
229 if (topic
== "nsPref:changed") {
231 this._pwmgr
.log("got change to " + prefName
+ " preference");
233 if (prefName
== "debug") {
235 this._pwmgr
._prefBranch
.getBoolPref("debug");
236 } else if (prefName
== "rememberSignons") {
237 this._pwmgr
._remember
=
238 this._pwmgr
._prefBranch
.getBoolPref("rememberSignons");
240 this._pwmgr
.log("Oops! Pref not handled, change ignored.");
242 } else if (topic
== "xpcom-shutdown") {
243 for (let i
in this._pwmgr
) {
245 this._pwmgr
[i
] = null;
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
: {
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
))
278 if (!this._pwmgr
._remember
)
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
))
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);
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
: {
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
);
333 case "DOMAutoComplete":
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);
347 this._pwmgr
.log("Oops, form changed before AC invoked");
352 this._pwmgr
.log("Oops! This event unexpected.");
361 /* ---------- Primary Public interfaces ---------- */
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.";
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
,
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
);
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
);
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
);
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
);
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
);
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
,
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.");
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
);
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
546 this.log("AutoCompleteSearch invoked. Search is: " + aSearchString
);
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);
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
);
599 /* ------- Internal methods / callbacks for document integration ------- */
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
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.
616 for (var i
= 0; i
< form
.elements
.length
; i
++) {
617 if (form
.elements
[i
].type
!= "password")
620 if (skipEmptyFields
&& !form
.elements
[i
].value
)
623 pwFields
[pwFields
.length
] = {
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.)");
633 } else if (pwFields
.length
> 3) {
634 this.log("(form ignored -- too many password fields. [got " +
635 pwFields
.length
+ "])");
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
);
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
];
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
;
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
721 // Treat as if 1 pw field
722 newPasswordField
= pwFields
[0].element
;
723 oldPasswordField
= null;
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")
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
);
767 var doc
= form
.ownerDocument
;
768 var win
= doc
.defaultView
;
770 // If password saving is disabled (globally or for host), bail out now.
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
+ ")");
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)
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
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)");
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)");
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
);
833 prompter
.promptToChangePasswordWithUsernames(
834 logins
, logins
.length
, formLogin
);
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.
862 same
= formLogin
.matches(login
, true);
866 existingLogin
= login
;
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
);
890 this.log("...passwords differ, prompting to change.");
891 prompter
= getPrompter(win
);
892 prompter
.promptToChangePassword(existingLogin
, formLogin
);
900 // Prompt user to save login (via dialog or notification bar)
901 prompter
= getPrompter(win
);
902 prompter
.promptToSavePassword(formLogin
);
909 * Get the parts of the URL we want for identification.
911 _getPasswordOrigin : function (uriString
, allowJS
) {
914 var uri
= this._ioService
.newURI(uriString
, null, null);
916 if (allowJS
&& uri
.scheme
== "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")
925 var handler
= this._ioService
.getProtocolHandler(uri
.scheme
);
926 if (port
!= handler
.defaultPort
)
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
);
940 _getActionOrigin : function (form
) {
941 var uriString
= form
.action
;
943 // A blank or mission action submits to where it came from.
945 uriString
= form
.baseURI
; // ala bug 297761
947 return this._getPasswordOrigin(uriString
, true);
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)
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))
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
++) {
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
) {
983 previousActionOrigin
= actionOrigin
;
985 this.log("_fillDocument processing form[" + i
+ "]");
986 foundLogins
= this._fillForm(form
, autofillForm
, false, foundLogins
)[1];
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
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
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) {
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.");
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
);
1051 this.log("Ignored " + l
.username
+ " login: won't fit");
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.
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();
1097 var found
= logins
.some(function(l
) {
1099 return (l
.username
.toLowerCase() == username
);
1102 selectedLogin
= matchingLogin
;
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];
1120 this.log("Multiple logins for form, so not filling any.");
1123 var didFillForm
= false;
1124 if (selectedLogin
&& autofillForm
&& !isFormDisabled
) {
1127 usernameField
.value
= selectedLogin
.username
;
1128 passwordField
.value
= selectedLogin
.password
;
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
];
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];
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();
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
]),
1209 // Interfaces from idl...
1210 searchString
: null,
1211 searchResult
: Ci
.nsIAutoCompleteResult
.RESULT_NOMATCH
,
1213 errorDescription
: "",
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
) {
1227 getStyleAt : function (index
) {
1231 getImageAt : function (index
) {
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);
1242 if (this.defaultIndex
> this.logins
.length
)
1243 this.defaultIndex
--;
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
);