|
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/. */ |
|
4 |
|
5 |
|
6 const Cc = Components.classes; |
|
7 const Ci = Components.interfaces; |
|
8 |
|
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"); |
|
12 |
|
13 XPCOMUtils.defineLazyModuleGetter(this, |
|
14 "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); |
|
15 |
|
16 var debug = false; |
|
17 function log(...pieces) { |
|
18 function generateLogMessage(args) { |
|
19 let strings = ['Login Manager:']; |
|
20 |
|
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 } |
|
38 |
|
39 if (!debug) |
|
40 return; |
|
41 |
|
42 let message = generateLogMessage(pieces); |
|
43 dump(message + "\n"); |
|
44 Services.console.logStringMessage(message); |
|
45 } |
|
46 |
|
47 function LoginManager() { |
|
48 this.init(); |
|
49 } |
|
50 |
|
51 LoginManager.prototype = { |
|
52 |
|
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 } |
|
62 |
|
63 throw Cr.NS_ERROR_NO_INTERFACE; |
|
64 }, |
|
65 |
|
66 |
|
67 /* ---------- private memebers ---------- */ |
|
68 |
|
69 |
|
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 }, |
|
78 |
|
79 |
|
80 __storage : null, // Storage component which contains the saved logins |
|
81 get _storage() { |
|
82 if (!this.__storage) { |
|
83 |
|
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 } |
|
94 |
|
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 } |
|
104 |
|
105 return this.__storage; |
|
106 }, |
|
107 |
|
108 _prefBranch : null, // Preferences service |
|
109 _remember : true, // mirrors signon.rememberSignons preference |
|
110 |
|
111 |
|
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 () { |
|
122 |
|
123 // Cache references to current |this| in utility objects |
|
124 this._observer._pwmgr = this; |
|
125 |
|
126 // Preferences. Add observer so we get notified of changes. |
|
127 this._prefBranch = Services.prefs.getBranch("signon."); |
|
128 this._prefBranch.addObserver("", this._observer, false); |
|
129 |
|
130 // Get current preference values. |
|
131 debug = this._prefBranch.getBoolPref("debug"); |
|
132 |
|
133 this._remember = this._prefBranch.getBoolPref("rememberSignons"); |
|
134 |
|
135 // Form submit observer checks forms for new logins and pw changes. |
|
136 Services.obs.addObserver(this._observer, "xpcom-shutdown", false); |
|
137 |
|
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 }, |
|
144 |
|
145 |
|
146 /* ---------- Utility objects ---------- */ |
|
147 |
|
148 |
|
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, |
|
157 |
|
158 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, |
|
159 Ci.nsISupportsWeakReference]), |
|
160 |
|
161 // nsObserver |
|
162 observe : function (subject, topic, data) { |
|
163 |
|
164 if (topic == "nsPref:changed") { |
|
165 var prefName = data; |
|
166 log("got change to", prefName, "preference"); |
|
167 |
|
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 }, |
|
188 |
|
189 |
|
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 }, |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 /* ---------- Primary Public interfaces ---------- */ |
|
204 |
|
205 |
|
206 |
|
207 |
|
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."; |
|
217 |
|
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."; |
|
221 |
|
222 if (login.password == null || login.password.length == 0) |
|
223 throw "Can't add a login with a null or empty password."; |
|
224 |
|
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 } |
|
237 |
|
238 |
|
239 // Look for an existing entry. |
|
240 var logins = this.findLogins({}, login.hostname, login.formSubmitURL, |
|
241 login.httpRealm); |
|
242 |
|
243 if (logins.some(function(l) login.matches(l, true))) |
|
244 throw "This login already exists."; |
|
245 |
|
246 log("Adding login"); |
|
247 return this._storage.addLogin(login); |
|
248 }, |
|
249 |
|
250 |
|
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 }, |
|
260 |
|
261 |
|
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 }, |
|
271 |
|
272 |
|
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 }, |
|
286 |
|
287 |
|
288 /* |
|
289 * removeAllLogins |
|
290 * |
|
291 * Remove all stored logins. |
|
292 */ |
|
293 removeAllLogins : function () { |
|
294 log("Removing all logins"); |
|
295 this._storage.removeAllLogins(); |
|
296 }, |
|
297 |
|
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 }, |
|
312 |
|
313 |
|
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); |
|
322 |
|
323 return this._storage.findLogins(count, hostname, formSubmitURL, |
|
324 httpRealm); |
|
325 }, |
|
326 |
|
327 |
|
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"); |
|
338 |
|
339 return this._storage.searchLogins(count, matchData); |
|
340 }, |
|
341 |
|
342 |
|
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); |
|
352 |
|
353 return this._storage.countLogins(hostname, formSubmitURL, httpRealm); |
|
354 }, |
|
355 |
|
356 |
|
357 /* |
|
358 * uiBusy |
|
359 */ |
|
360 get uiBusy() { |
|
361 return this._storage.uiBusy; |
|
362 }, |
|
363 |
|
364 |
|
365 /* |
|
366 * isLoggedIn |
|
367 */ |
|
368 get isLoggedIn() { |
|
369 return this._storage.isLoggedIn; |
|
370 }, |
|
371 |
|
372 |
|
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; |
|
382 |
|
383 return this._storage.getLoginSavingEnabled(host); |
|
384 }, |
|
385 |
|
386 |
|
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"; |
|
396 |
|
397 log("Login saving for", hostname, "now enabled?", enabled); |
|
398 return this._storage.setLoginSavingEnabled(hostname, enabled); |
|
399 }, |
|
400 |
|
401 |
|
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 |
|
415 |
|
416 if (!this._remember) |
|
417 return null; |
|
418 |
|
419 log("AutoCompleteSearch invoked. Search is:", aSearchString); |
|
420 |
|
421 var result = null; |
|
422 |
|
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; |
|
428 |
|
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); |
|
435 |
|
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."); |
|
447 |
|
448 var doc = aElement.ownerDocument; |
|
449 var origin = this._getPasswordOrigin(doc.documentURI); |
|
450 var actionOrigin = this._getActionOrigin(aElement.form); |
|
451 |
|
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 = []; |
|
457 |
|
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 } |
|
474 |
|
475 return result; |
|
476 }, |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 /* ------- Internal methods / callbacks for document integration ------- */ |
|
482 |
|
483 |
|
484 |
|
485 |
|
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); |
|
495 |
|
496 if (allowJS && uri.scheme == "javascript") |
|
497 return "javascript:" |
|
498 |
|
499 realm = uri.scheme + "://" + uri.host; |
|
500 |
|
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 } |
|
509 |
|
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 } |
|
516 |
|
517 return realm; |
|
518 }, |
|
519 |
|
520 _getActionOrigin : function (form) { |
|
521 var uriString = form.action; |
|
522 |
|
523 // A blank or missing action submits to where it came from. |
|
524 if (uriString == "") |
|
525 uriString = form.baseURI; // ala bug 297761 |
|
526 |
|
527 return this._getPasswordOrigin(uriString, true); |
|
528 }, |
|
529 |
|
530 |
|
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 }, |
|
540 |
|
541 }; // end of LoginManager implementation |
|
542 |
|
543 |
|
544 |
|
545 |
|
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(); |
|
551 |
|
552 if (userA < userB) |
|
553 return -1; |
|
554 |
|
555 if (userB > userA) |
|
556 return 1; |
|
557 |
|
558 return 0; |
|
559 }; |
|
560 |
|
561 this.searchString = aSearchString; |
|
562 this.logins = matchingLogins.sort(loginSort); |
|
563 this.matchCount = matchingLogins.length; |
|
564 |
|
565 if (this.matchCount > 0) { |
|
566 this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; |
|
567 this.defaultIndex = 0; |
|
568 } |
|
569 } |
|
570 |
|
571 UserAutoCompleteResult.prototype = { |
|
572 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, |
|
573 Ci.nsISupportsWeakReference]), |
|
574 |
|
575 // private |
|
576 logins : null, |
|
577 |
|
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 }, |
|
583 |
|
584 // Interfaces from idl... |
|
585 searchString : null, |
|
586 searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, |
|
587 defaultIndex : -1, |
|
588 errorDescription : "", |
|
589 matchCount : 0, |
|
590 |
|
591 getValueAt : function (index) { |
|
592 if (index < 0 || index >= this.logins.length) |
|
593 throw "Index out of range."; |
|
594 |
|
595 return this.logins[index].username; |
|
596 }, |
|
597 |
|
598 getLabelAt: function(index) { |
|
599 return this.getValueAt(index); |
|
600 }, |
|
601 |
|
602 getCommentAt : function (index) { |
|
603 return ""; |
|
604 }, |
|
605 |
|
606 getStyleAt : function (index) { |
|
607 return ""; |
|
608 }, |
|
609 |
|
610 getImageAt : function (index) { |
|
611 return ""; |
|
612 }, |
|
613 |
|
614 getFinalCompleteValueAt : function (index) { |
|
615 return this.getValueAt(index); |
|
616 }, |
|
617 |
|
618 removeValueAt : function (index, removeFromDB) { |
|
619 if (index < 0 || index >= this.logins.length) |
|
620 throw "Index out of range."; |
|
621 |
|
622 var [removedLogin] = this.logins.splice(index, 1); |
|
623 |
|
624 this.matchCount--; |
|
625 if (this.defaultIndex > this.logins.length) |
|
626 this.defaultIndex--; |
|
627 |
|
628 if (removeFromDB) { |
|
629 var pwmgr = Cc["@mozilla.org/login-manager;1"]. |
|
630 getService(Ci.nsILoginManager); |
|
631 pwmgr.removeLogin(removedLogin); |
|
632 } |
|
633 } |
|
634 }; |
|
635 |
|
636 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]); |