Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const Cc = Components.classes;
7 const Ci = Components.interfaces;
9 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
10 Components.utils.import("resource://gre/modules/Services.jsm");
11 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
13 XPCOMUtils.defineLazyModuleGetter(this,
14 "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
16 var debug = false;
17 function log(...pieces) {
18 function generateLogMessage(args) {
19 let strings = ['Login Manager:'];
21 args.forEach(function(arg) {
22 if (typeof arg === 'string') {
23 strings.push(arg);
24 } else if (typeof arg === 'undefined') {
25 strings.push('undefined');
26 } else if (arg === null) {
27 strings.push('null');
28 } else {
29 try {
30 strings.push(JSON.stringify(arg, null, 2));
31 } catch(err) {
32 strings.push("<<something>>");
33 }
34 }
35 });
36 return strings.join(' ');
37 }
39 if (!debug)
40 return;
42 let message = generateLogMessage(pieces);
43 dump(message + "\n");
44 Services.console.logStringMessage(message);
45 }
47 function LoginManager() {
48 this.init();
49 }
51 LoginManager.prototype = {
53 classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
54 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
55 Ci.nsISupportsWeakReference,
56 Ci.nsIInterfaceRequestor]),
57 getInterface : function(aIID) {
58 if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
59 let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
60 return ir.getInterface(aIID);
61 }
63 throw Cr.NS_ERROR_NO_INTERFACE;
64 },
67 /* ---------- private memebers ---------- */
70 __formFillService : null, // FormFillController, for username autocompleting
71 get _formFillService() {
72 if (!this.__formFillService)
73 this.__formFillService =
74 Cc["@mozilla.org/satchel/form-fill-controller;1"].
75 getService(Ci.nsIFormFillController);
76 return this.__formFillService;
77 },
80 __storage : null, // Storage component which contains the saved logins
81 get _storage() {
82 if (!this.__storage) {
84 var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
85 try {
86 var catMan = Cc["@mozilla.org/categorymanager;1"].
87 getService(Ci.nsICategoryManager);
88 contractID = catMan.getCategoryEntry("login-manager-storage",
89 "nsILoginManagerStorage");
90 log("Found alternate nsILoginManagerStorage with contract ID:", contractID);
91 } catch (e) {
92 log("No alternate nsILoginManagerStorage registered");
93 }
95 this.__storage = Cc[contractID].
96 createInstance(Ci.nsILoginManagerStorage);
97 try {
98 this.__storage.init();
99 } catch (e) {
100 log("Initialization of storage component failed:", e);
101 this.__storage = null;
102 }
103 }
105 return this.__storage;
106 },
108 _prefBranch : null, // Preferences service
109 _remember : true, // mirrors signon.rememberSignons preference
112 /*
113 * init
114 *
115 * Initialize the Login Manager. Automatically called when service
116 * is created.
117 *
118 * Note: Service created in /browser/base/content/browser.js,
119 * delayedStartup()
120 */
121 init : function () {
123 // Cache references to current |this| in utility objects
124 this._observer._pwmgr = this;
126 // Preferences. Add observer so we get notified of changes.
127 this._prefBranch = Services.prefs.getBranch("signon.");
128 this._prefBranch.addObserver("", this._observer, false);
130 // Get current preference values.
131 debug = this._prefBranch.getBoolPref("debug");
133 this._remember = this._prefBranch.getBoolPref("rememberSignons");
135 // Form submit observer checks forms for new logins and pw changes.
136 Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
138 // XXX gross hacky workaround for bug 881996. The WPL does nothing.
139 var progress = Cc["@mozilla.org/docloaderservice;1"].
140 getService(Ci.nsIWebProgress);
141 progress.addProgressListener(this._webProgressListener,
142 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
143 },
146 /* ---------- Utility objects ---------- */
149 /*
150 * _observer object
151 *
152 * Internal utility object, implements the nsIObserver interface.
153 * Used to receive notification for: form submission, preference changes.
154 */
155 _observer : {
156 _pwmgr : null,
158 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
159 Ci.nsISupportsWeakReference]),
161 // nsObserver
162 observe : function (subject, topic, data) {
164 if (topic == "nsPref:changed") {
165 var prefName = data;
166 log("got change to", prefName, "preference");
168 if (prefName == "debug") {
169 debug = this._pwmgr._prefBranch.getBoolPref("debug");
170 } else if (prefName == "rememberSignons") {
171 this._pwmgr._remember =
172 this._pwmgr._prefBranch.getBoolPref("rememberSignons");
173 } else {
174 log("Oops! Pref not handled, change ignored.");
175 }
176 } else if (topic == "xpcom-shutdown") {
177 for (let i in this._pwmgr) {
178 try {
179 this._pwmgr[i] = null;
180 } catch(ex) {}
181 }
182 this._pwmgr = null;
183 } else {
184 log("Oops! Unexpected notification:", topic);
185 }
186 }
187 },
190 _webProgressListener : {
191 QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
192 Ci.nsISupportsWeakReference]),
193 onStateChange : function() { /* NOP */ },
194 onProgressChange : function() { throw "Unexpected onProgressChange"; },
195 onLocationChange : function() { throw "Unexpected onLocationChange"; },
196 onStatusChange : function() { throw "Unexpected onStatusChange"; },
197 onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
198 },
203 /* ---------- Primary Public interfaces ---------- */
208 /*
209 * addLogin
210 *
211 * Add a new login to login storage.
212 */
213 addLogin : function (login) {
214 // Sanity check the login
215 if (login.hostname == null || login.hostname.length == 0)
216 throw "Can't add a login with a null or empty hostname.";
218 // For logins w/o a username, set to "", not null.
219 if (login.username == null)
220 throw "Can't add a login with a null username.";
222 if (login.password == null || login.password.length == 0)
223 throw "Can't add a login with a null or empty password.";
225 if (login.formSubmitURL || login.formSubmitURL == "") {
226 // We have a form submit URL. Can't have a HTTP realm.
227 if (login.httpRealm != null)
228 throw "Can't add a login with both a httpRealm and formSubmitURL.";
229 } else if (login.httpRealm) {
230 // We have a HTTP realm. Can't have a form submit URL.
231 if (login.formSubmitURL != null)
232 throw "Can't add a login with both a httpRealm and formSubmitURL.";
233 } else {
234 // Need one or the other!
235 throw "Can't add a login without a httpRealm or formSubmitURL.";
236 }
239 // Look for an existing entry.
240 var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
241 login.httpRealm);
243 if (logins.some(function(l) login.matches(l, true)))
244 throw "This login already exists.";
246 log("Adding login");
247 return this._storage.addLogin(login);
248 },
251 /*
252 * removeLogin
253 *
254 * Remove the specified login from the stored logins.
255 */
256 removeLogin : function (login) {
257 log("Removing login");
258 return this._storage.removeLogin(login);
259 },
262 /*
263 * modifyLogin
264 *
265 * Change the specified login to match the new login.
266 */
267 modifyLogin : function (oldLogin, newLogin) {
268 log("Modifying login");
269 return this._storage.modifyLogin(oldLogin, newLogin);
270 },
273 /*
274 * getAllLogins
275 *
276 * Get a dump of all stored logins. Used by the login manager UI.
277 *
278 * |count| is only needed for XPCOM.
279 *
280 * Returns an array of logins. If there are no logins, the array is empty.
281 */
282 getAllLogins : function (count) {
283 log("Getting a list of all logins");
284 return this._storage.getAllLogins(count);
285 },
288 /*
289 * removeAllLogins
290 *
291 * Remove all stored logins.
292 */
293 removeAllLogins : function () {
294 log("Removing all logins");
295 this._storage.removeAllLogins();
296 },
298 /*
299 * getAllDisabledHosts
300 *
301 * Get a list of all hosts for which logins are disabled.
302 *
303 * |count| is only needed for XPCOM.
304 *
305 * Returns an array of disabled logins. If there are no disabled logins,
306 * the array is empty.
307 */
308 getAllDisabledHosts : function (count) {
309 log("Getting a list of all disabled hosts");
310 return this._storage.getAllDisabledHosts(count);
311 },
314 /*
315 * findLogins
316 *
317 * Search for the known logins for entries matching the specified criteria.
318 */
319 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
320 log("Searching for logins matching host:", hostname,
321 "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm);
323 return this._storage.findLogins(count, hostname, formSubmitURL,
324 httpRealm);
325 },
328 /*
329 * searchLogins
330 *
331 * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
332 * JavaScript object and decrypt the results.
333 *
334 * Returns an array of decrypted nsILoginInfo.
335 */
336 searchLogins : function(count, matchData) {
337 log("Searching for logins");
339 return this._storage.searchLogins(count, matchData);
340 },
343 /*
344 * countLogins
345 *
346 * Search for the known logins for entries matching the specified criteria,
347 * returns only the count.
348 */
349 countLogins : function (hostname, formSubmitURL, httpRealm) {
350 log("Counting logins matching host:", hostname,
351 "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm);
353 return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
354 },
357 /*
358 * uiBusy
359 */
360 get uiBusy() {
361 return this._storage.uiBusy;
362 },
365 /*
366 * isLoggedIn
367 */
368 get isLoggedIn() {
369 return this._storage.isLoggedIn;
370 },
373 /*
374 * getLoginSavingEnabled
375 *
376 * Check to see if user has disabled saving logins for the host.
377 */
378 getLoginSavingEnabled : function (host) {
379 log("Checking if logins to", host, "can be saved.");
380 if (!this._remember)
381 return false;
383 return this._storage.getLoginSavingEnabled(host);
384 },
387 /*
388 * setLoginSavingEnabled
389 *
390 * Enable or disable storing logins for the specified host.
391 */
392 setLoginSavingEnabled : function (hostname, enabled) {
393 // Nulls won't round-trip with getAllDisabledHosts().
394 if (hostname.indexOf("\0") != -1)
395 throw "Invalid hostname";
397 log("Login saving for", hostname, "now enabled?", enabled);
398 return this._storage.setLoginSavingEnabled(hostname, enabled);
399 },
402 /*
403 * autoCompleteSearch
404 *
405 * Yuck. This is called directly by satchel:
406 * nsFormFillController::StartSearch()
407 * [toolkit/components/satchel/src/nsFormFillController.cpp]
408 *
409 * We really ought to have a simple way for code to register an
410 * auto-complete provider, and not have satchel calling pwmgr directly.
411 */
412 autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
413 // aPreviousResult & aResult are nsIAutoCompleteResult,
414 // aElement is nsIDOMHTMLInputElement
416 if (!this._remember)
417 return null;
419 log("AutoCompleteSearch invoked. Search is:", aSearchString);
421 var result = null;
423 if (aPreviousResult &&
424 aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) {
425 log("Using previous autocomplete result");
426 result = aPreviousResult;
427 result.wrappedJSObject.searchString = aSearchString;
429 // We have a list of results for a shorter search string, so just
430 // filter them further based on the new search string.
431 // Count backwards, because result.matchCount is decremented
432 // when we remove an entry.
433 for (var i = result.matchCount - 1; i >= 0; i--) {
434 var match = result.getValueAt(i);
436 // Remove results that are too short, or have different prefix.
437 if (aSearchString.length > match.length ||
438 aSearchString.toLowerCase() !=
439 match.substr(0, aSearchString.length).toLowerCase())
440 {
441 log("Removing autocomplete entry:", match);
442 result.removeValueAt(i, false);
443 }
444 }
445 } else {
446 log("Creating new autocomplete search result.");
448 var doc = aElement.ownerDocument;
449 var origin = this._getPasswordOrigin(doc.documentURI);
450 var actionOrigin = this._getActionOrigin(aElement.form);
452 // This shouldn't trigger a master password prompt, because we
453 // don't attach to the input until after we successfully obtain
454 // logins for the form.
455 var logins = this.findLogins({}, origin, actionOrigin, null);
456 var matchingLogins = [];
458 // Filter out logins that don't match the search prefix. Also
459 // filter logins without a username, since that's confusing to see
460 // in the dropdown and we can't autocomplete them anyway.
461 for (i = 0; i < logins.length; i++) {
462 var username = logins[i].username.toLowerCase();
463 if (username &&
464 aSearchString.length <= username.length &&
465 aSearchString.toLowerCase() ==
466 username.substr(0, aSearchString.length))
467 {
468 matchingLogins.push(logins[i]);
469 }
470 }
471 log(matchingLogins.length, "autocomplete logins avail.");
472 result = new UserAutoCompleteResult(aSearchString, matchingLogins);
473 }
475 return result;
476 },
481 /* ------- Internal methods / callbacks for document integration ------- */
486 /*
487 * _getPasswordOrigin
488 *
489 * Get the parts of the URL we want for identification.
490 */
491 _getPasswordOrigin : function (uriString, allowJS) {
492 var realm = "";
493 try {
494 var uri = Services.io.newURI(uriString, null, null);
496 if (allowJS && uri.scheme == "javascript")
497 return "javascript:"
499 realm = uri.scheme + "://" + uri.host;
501 // If the URI explicitly specified a port, only include it when
502 // it's not the default. (We never want "http://foo.com:80")
503 var port = uri.port;
504 if (port != -1) {
505 var handler = Services.io.getProtocolHandler(uri.scheme);
506 if (port != handler.defaultPort)
507 realm += ":" + port;
508 }
510 } catch (e) {
511 // bug 159484 - disallow url types that don't support a hostPort.
512 // (although we handle "javascript:..." as a special case above.)
513 log("Couldn't parse origin for", uriString);
514 realm = null;
515 }
517 return realm;
518 },
520 _getActionOrigin : function (form) {
521 var uriString = form.action;
523 // A blank or missing action submits to where it came from.
524 if (uriString == "")
525 uriString = form.baseURI; // ala bug 297761
527 return this._getPasswordOrigin(uriString, true);
528 },
531 /*
532 * fillForm
533 *
534 * Fill the form with login information if we can find it.
535 */
536 fillForm : function (form) {
537 log("fillForm processing form[ id:", form.id, "]");
538 return LoginManagerContent._fillForm(form, true, true, false, null)[0];
539 },
541 }; // end of LoginManager implementation
546 // nsIAutoCompleteResult implementation
547 function UserAutoCompleteResult (aSearchString, matchingLogins) {
548 function loginSort(a,b) {
549 var userA = a.username.toLowerCase();
550 var userB = b.username.toLowerCase();
552 if (userA < userB)
553 return -1;
555 if (userB > userA)
556 return 1;
558 return 0;
559 };
561 this.searchString = aSearchString;
562 this.logins = matchingLogins.sort(loginSort);
563 this.matchCount = matchingLogins.length;
565 if (this.matchCount > 0) {
566 this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
567 this.defaultIndex = 0;
568 }
569 }
571 UserAutoCompleteResult.prototype = {
572 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
573 Ci.nsISupportsWeakReference]),
575 // private
576 logins : null,
578 // Allow autoCompleteSearch to get at the JS object so it can
579 // modify some readonly properties for internal use.
580 get wrappedJSObject() {
581 return this;
582 },
584 // Interfaces from idl...
585 searchString : null,
586 searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
587 defaultIndex : -1,
588 errorDescription : "",
589 matchCount : 0,
591 getValueAt : function (index) {
592 if (index < 0 || index >= this.logins.length)
593 throw "Index out of range.";
595 return this.logins[index].username;
596 },
598 getLabelAt: function(index) {
599 return this.getValueAt(index);
600 },
602 getCommentAt : function (index) {
603 return "";
604 },
606 getStyleAt : function (index) {
607 return "";
608 },
610 getImageAt : function (index) {
611 return "";
612 },
614 getFinalCompleteValueAt : function (index) {
615 return this.getValueAt(index);
616 },
618 removeValueAt : function (index, removeFromDB) {
619 if (index < 0 || index >= this.logins.length)
620 throw "Index out of range.";
622 var [removedLogin] = this.logins.splice(index, 1);
624 this.matchCount--;
625 if (this.defaultIndex > this.logins.length)
626 this.defaultIndex--;
628 if (removeFromDB) {
629 var pwmgr = Cc["@mozilla.org/login-manager;1"].
630 getService(Ci.nsILoginManager);
631 pwmgr.removeLogin(removedLogin);
632 }
633 }
634 };
636 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);