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