1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/passwordmgr/nsLoginManager.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,636 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 + 1.9 +const Cc = Components.classes; 1.10 +const Ci = Components.interfaces; 1.11 + 1.12 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.13 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.14 +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.15 + 1.16 +XPCOMUtils.defineLazyModuleGetter(this, 1.17 + "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); 1.18 + 1.19 +var debug = false; 1.20 +function log(...pieces) { 1.21 + function generateLogMessage(args) { 1.22 + let strings = ['Login Manager:']; 1.23 + 1.24 + args.forEach(function(arg) { 1.25 + if (typeof arg === 'string') { 1.26 + strings.push(arg); 1.27 + } else if (typeof arg === 'undefined') { 1.28 + strings.push('undefined'); 1.29 + } else if (arg === null) { 1.30 + strings.push('null'); 1.31 + } else { 1.32 + try { 1.33 + strings.push(JSON.stringify(arg, null, 2)); 1.34 + } catch(err) { 1.35 + strings.push("<<something>>"); 1.36 + } 1.37 + } 1.38 + }); 1.39 + return strings.join(' '); 1.40 + } 1.41 + 1.42 + if (!debug) 1.43 + return; 1.44 + 1.45 + let message = generateLogMessage(pieces); 1.46 + dump(message + "\n"); 1.47 + Services.console.logStringMessage(message); 1.48 +} 1.49 + 1.50 +function LoginManager() { 1.51 + this.init(); 1.52 +} 1.53 + 1.54 +LoginManager.prototype = { 1.55 + 1.56 + classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), 1.57 + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, 1.58 + Ci.nsISupportsWeakReference, 1.59 + Ci.nsIInterfaceRequestor]), 1.60 + getInterface : function(aIID) { 1.61 + if (aIID.equals(Ci.mozIStorageConnection) && this._storage) { 1.62 + let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor); 1.63 + return ir.getInterface(aIID); 1.64 + } 1.65 + 1.66 + throw Cr.NS_ERROR_NO_INTERFACE; 1.67 + }, 1.68 + 1.69 + 1.70 + /* ---------- private memebers ---------- */ 1.71 + 1.72 + 1.73 + __formFillService : null, // FormFillController, for username autocompleting 1.74 + get _formFillService() { 1.75 + if (!this.__formFillService) 1.76 + this.__formFillService = 1.77 + Cc["@mozilla.org/satchel/form-fill-controller;1"]. 1.78 + getService(Ci.nsIFormFillController); 1.79 + return this.__formFillService; 1.80 + }, 1.81 + 1.82 + 1.83 + __storage : null, // Storage component which contains the saved logins 1.84 + get _storage() { 1.85 + if (!this.__storage) { 1.86 + 1.87 + var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; 1.88 + try { 1.89 + var catMan = Cc["@mozilla.org/categorymanager;1"]. 1.90 + getService(Ci.nsICategoryManager); 1.91 + contractID = catMan.getCategoryEntry("login-manager-storage", 1.92 + "nsILoginManagerStorage"); 1.93 + log("Found alternate nsILoginManagerStorage with contract ID:", contractID); 1.94 + } catch (e) { 1.95 + log("No alternate nsILoginManagerStorage registered"); 1.96 + } 1.97 + 1.98 + this.__storage = Cc[contractID]. 1.99 + createInstance(Ci.nsILoginManagerStorage); 1.100 + try { 1.101 + this.__storage.init(); 1.102 + } catch (e) { 1.103 + log("Initialization of storage component failed:", e); 1.104 + this.__storage = null; 1.105 + } 1.106 + } 1.107 + 1.108 + return this.__storage; 1.109 + }, 1.110 + 1.111 + _prefBranch : null, // Preferences service 1.112 + _remember : true, // mirrors signon.rememberSignons preference 1.113 + 1.114 + 1.115 + /* 1.116 + * init 1.117 + * 1.118 + * Initialize the Login Manager. Automatically called when service 1.119 + * is created. 1.120 + * 1.121 + * Note: Service created in /browser/base/content/browser.js, 1.122 + * delayedStartup() 1.123 + */ 1.124 + init : function () { 1.125 + 1.126 + // Cache references to current |this| in utility objects 1.127 + this._observer._pwmgr = this; 1.128 + 1.129 + // Preferences. Add observer so we get notified of changes. 1.130 + this._prefBranch = Services.prefs.getBranch("signon."); 1.131 + this._prefBranch.addObserver("", this._observer, false); 1.132 + 1.133 + // Get current preference values. 1.134 + debug = this._prefBranch.getBoolPref("debug"); 1.135 + 1.136 + this._remember = this._prefBranch.getBoolPref("rememberSignons"); 1.137 + 1.138 + // Form submit observer checks forms for new logins and pw changes. 1.139 + Services.obs.addObserver(this._observer, "xpcom-shutdown", false); 1.140 + 1.141 + // XXX gross hacky workaround for bug 881996. The WPL does nothing. 1.142 + var progress = Cc["@mozilla.org/docloaderservice;1"]. 1.143 + getService(Ci.nsIWebProgress); 1.144 + progress.addProgressListener(this._webProgressListener, 1.145 + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); 1.146 + }, 1.147 + 1.148 + 1.149 + /* ---------- Utility objects ---------- */ 1.150 + 1.151 + 1.152 + /* 1.153 + * _observer object 1.154 + * 1.155 + * Internal utility object, implements the nsIObserver interface. 1.156 + * Used to receive notification for: form submission, preference changes. 1.157 + */ 1.158 + _observer : { 1.159 + _pwmgr : null, 1.160 + 1.161 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 1.162 + Ci.nsISupportsWeakReference]), 1.163 + 1.164 + // nsObserver 1.165 + observe : function (subject, topic, data) { 1.166 + 1.167 + if (topic == "nsPref:changed") { 1.168 + var prefName = data; 1.169 + log("got change to", prefName, "preference"); 1.170 + 1.171 + if (prefName == "debug") { 1.172 + debug = this._pwmgr._prefBranch.getBoolPref("debug"); 1.173 + } else if (prefName == "rememberSignons") { 1.174 + this._pwmgr._remember = 1.175 + this._pwmgr._prefBranch.getBoolPref("rememberSignons"); 1.176 + } else { 1.177 + log("Oops! Pref not handled, change ignored."); 1.178 + } 1.179 + } else if (topic == "xpcom-shutdown") { 1.180 + for (let i in this._pwmgr) { 1.181 + try { 1.182 + this._pwmgr[i] = null; 1.183 + } catch(ex) {} 1.184 + } 1.185 + this._pwmgr = null; 1.186 + } else { 1.187 + log("Oops! Unexpected notification:", topic); 1.188 + } 1.189 + } 1.190 + }, 1.191 + 1.192 + 1.193 + _webProgressListener : { 1.194 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, 1.195 + Ci.nsISupportsWeakReference]), 1.196 + onStateChange : function() { /* NOP */ }, 1.197 + onProgressChange : function() { throw "Unexpected onProgressChange"; }, 1.198 + onLocationChange : function() { throw "Unexpected onLocationChange"; }, 1.199 + onStatusChange : function() { throw "Unexpected onStatusChange"; }, 1.200 + onSecurityChange : function() { throw "Unexpected onSecurityChange"; } 1.201 + }, 1.202 + 1.203 + 1.204 + 1.205 + 1.206 + /* ---------- Primary Public interfaces ---------- */ 1.207 + 1.208 + 1.209 + 1.210 + 1.211 + /* 1.212 + * addLogin 1.213 + * 1.214 + * Add a new login to login storage. 1.215 + */ 1.216 + addLogin : function (login) { 1.217 + // Sanity check the login 1.218 + if (login.hostname == null || login.hostname.length == 0) 1.219 + throw "Can't add a login with a null or empty hostname."; 1.220 + 1.221 + // For logins w/o a username, set to "", not null. 1.222 + if (login.username == null) 1.223 + throw "Can't add a login with a null username."; 1.224 + 1.225 + if (login.password == null || login.password.length == 0) 1.226 + throw "Can't add a login with a null or empty password."; 1.227 + 1.228 + if (login.formSubmitURL || login.formSubmitURL == "") { 1.229 + // We have a form submit URL. Can't have a HTTP realm. 1.230 + if (login.httpRealm != null) 1.231 + throw "Can't add a login with both a httpRealm and formSubmitURL."; 1.232 + } else if (login.httpRealm) { 1.233 + // We have a HTTP realm. Can't have a form submit URL. 1.234 + if (login.formSubmitURL != null) 1.235 + throw "Can't add a login with both a httpRealm and formSubmitURL."; 1.236 + } else { 1.237 + // Need one or the other! 1.238 + throw "Can't add a login without a httpRealm or formSubmitURL."; 1.239 + } 1.240 + 1.241 + 1.242 + // Look for an existing entry. 1.243 + var logins = this.findLogins({}, login.hostname, login.formSubmitURL, 1.244 + login.httpRealm); 1.245 + 1.246 + if (logins.some(function(l) login.matches(l, true))) 1.247 + throw "This login already exists."; 1.248 + 1.249 + log("Adding login"); 1.250 + return this._storage.addLogin(login); 1.251 + }, 1.252 + 1.253 + 1.254 + /* 1.255 + * removeLogin 1.256 + * 1.257 + * Remove the specified login from the stored logins. 1.258 + */ 1.259 + removeLogin : function (login) { 1.260 + log("Removing login"); 1.261 + return this._storage.removeLogin(login); 1.262 + }, 1.263 + 1.264 + 1.265 + /* 1.266 + * modifyLogin 1.267 + * 1.268 + * Change the specified login to match the new login. 1.269 + */ 1.270 + modifyLogin : function (oldLogin, newLogin) { 1.271 + log("Modifying login"); 1.272 + return this._storage.modifyLogin(oldLogin, newLogin); 1.273 + }, 1.274 + 1.275 + 1.276 + /* 1.277 + * getAllLogins 1.278 + * 1.279 + * Get a dump of all stored logins. Used by the login manager UI. 1.280 + * 1.281 + * |count| is only needed for XPCOM. 1.282 + * 1.283 + * Returns an array of logins. If there are no logins, the array is empty. 1.284 + */ 1.285 + getAllLogins : function (count) { 1.286 + log("Getting a list of all logins"); 1.287 + return this._storage.getAllLogins(count); 1.288 + }, 1.289 + 1.290 + 1.291 + /* 1.292 + * removeAllLogins 1.293 + * 1.294 + * Remove all stored logins. 1.295 + */ 1.296 + removeAllLogins : function () { 1.297 + log("Removing all logins"); 1.298 + this._storage.removeAllLogins(); 1.299 + }, 1.300 + 1.301 + /* 1.302 + * getAllDisabledHosts 1.303 + * 1.304 + * Get a list of all hosts for which logins are disabled. 1.305 + * 1.306 + * |count| is only needed for XPCOM. 1.307 + * 1.308 + * Returns an array of disabled logins. If there are no disabled logins, 1.309 + * the array is empty. 1.310 + */ 1.311 + getAllDisabledHosts : function (count) { 1.312 + log("Getting a list of all disabled hosts"); 1.313 + return this._storage.getAllDisabledHosts(count); 1.314 + }, 1.315 + 1.316 + 1.317 + /* 1.318 + * findLogins 1.319 + * 1.320 + * Search for the known logins for entries matching the specified criteria. 1.321 + */ 1.322 + findLogins : function (count, hostname, formSubmitURL, httpRealm) { 1.323 + log("Searching for logins matching host:", hostname, 1.324 + "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm); 1.325 + 1.326 + return this._storage.findLogins(count, hostname, formSubmitURL, 1.327 + httpRealm); 1.328 + }, 1.329 + 1.330 + 1.331 + /* 1.332 + * searchLogins 1.333 + * 1.334 + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a 1.335 + * JavaScript object and decrypt the results. 1.336 + * 1.337 + * Returns an array of decrypted nsILoginInfo. 1.338 + */ 1.339 + searchLogins : function(count, matchData) { 1.340 + log("Searching for logins"); 1.341 + 1.342 + return this._storage.searchLogins(count, matchData); 1.343 + }, 1.344 + 1.345 + 1.346 + /* 1.347 + * countLogins 1.348 + * 1.349 + * Search for the known logins for entries matching the specified criteria, 1.350 + * returns only the count. 1.351 + */ 1.352 + countLogins : function (hostname, formSubmitURL, httpRealm) { 1.353 + log("Counting logins matching host:", hostname, 1.354 + "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm); 1.355 + 1.356 + return this._storage.countLogins(hostname, formSubmitURL, httpRealm); 1.357 + }, 1.358 + 1.359 + 1.360 + /* 1.361 + * uiBusy 1.362 + */ 1.363 + get uiBusy() { 1.364 + return this._storage.uiBusy; 1.365 + }, 1.366 + 1.367 + 1.368 + /* 1.369 + * isLoggedIn 1.370 + */ 1.371 + get isLoggedIn() { 1.372 + return this._storage.isLoggedIn; 1.373 + }, 1.374 + 1.375 + 1.376 + /* 1.377 + * getLoginSavingEnabled 1.378 + * 1.379 + * Check to see if user has disabled saving logins for the host. 1.380 + */ 1.381 + getLoginSavingEnabled : function (host) { 1.382 + log("Checking if logins to", host, "can be saved."); 1.383 + if (!this._remember) 1.384 + return false; 1.385 + 1.386 + return this._storage.getLoginSavingEnabled(host); 1.387 + }, 1.388 + 1.389 + 1.390 + /* 1.391 + * setLoginSavingEnabled 1.392 + * 1.393 + * Enable or disable storing logins for the specified host. 1.394 + */ 1.395 + setLoginSavingEnabled : function (hostname, enabled) { 1.396 + // Nulls won't round-trip with getAllDisabledHosts(). 1.397 + if (hostname.indexOf("\0") != -1) 1.398 + throw "Invalid hostname"; 1.399 + 1.400 + log("Login saving for", hostname, "now enabled?", enabled); 1.401 + return this._storage.setLoginSavingEnabled(hostname, enabled); 1.402 + }, 1.403 + 1.404 + 1.405 + /* 1.406 + * autoCompleteSearch 1.407 + * 1.408 + * Yuck. This is called directly by satchel: 1.409 + * nsFormFillController::StartSearch() 1.410 + * [toolkit/components/satchel/src/nsFormFillController.cpp] 1.411 + * 1.412 + * We really ought to have a simple way for code to register an 1.413 + * auto-complete provider, and not have satchel calling pwmgr directly. 1.414 + */ 1.415 + autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { 1.416 + // aPreviousResult & aResult are nsIAutoCompleteResult, 1.417 + // aElement is nsIDOMHTMLInputElement 1.418 + 1.419 + if (!this._remember) 1.420 + return null; 1.421 + 1.422 + log("AutoCompleteSearch invoked. Search is:", aSearchString); 1.423 + 1.424 + var result = null; 1.425 + 1.426 + if (aPreviousResult && 1.427 + aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) { 1.428 + log("Using previous autocomplete result"); 1.429 + result = aPreviousResult; 1.430 + result.wrappedJSObject.searchString = aSearchString; 1.431 + 1.432 + // We have a list of results for a shorter search string, so just 1.433 + // filter them further based on the new search string. 1.434 + // Count backwards, because result.matchCount is decremented 1.435 + // when we remove an entry. 1.436 + for (var i = result.matchCount - 1; i >= 0; i--) { 1.437 + var match = result.getValueAt(i); 1.438 + 1.439 + // Remove results that are too short, or have different prefix. 1.440 + if (aSearchString.length > match.length || 1.441 + aSearchString.toLowerCase() != 1.442 + match.substr(0, aSearchString.length).toLowerCase()) 1.443 + { 1.444 + log("Removing autocomplete entry:", match); 1.445 + result.removeValueAt(i, false); 1.446 + } 1.447 + } 1.448 + } else { 1.449 + log("Creating new autocomplete search result."); 1.450 + 1.451 + var doc = aElement.ownerDocument; 1.452 + var origin = this._getPasswordOrigin(doc.documentURI); 1.453 + var actionOrigin = this._getActionOrigin(aElement.form); 1.454 + 1.455 + // This shouldn't trigger a master password prompt, because we 1.456 + // don't attach to the input until after we successfully obtain 1.457 + // logins for the form. 1.458 + var logins = this.findLogins({}, origin, actionOrigin, null); 1.459 + var matchingLogins = []; 1.460 + 1.461 + // Filter out logins that don't match the search prefix. Also 1.462 + // filter logins without a username, since that's confusing to see 1.463 + // in the dropdown and we can't autocomplete them anyway. 1.464 + for (i = 0; i < logins.length; i++) { 1.465 + var username = logins[i].username.toLowerCase(); 1.466 + if (username && 1.467 + aSearchString.length <= username.length && 1.468 + aSearchString.toLowerCase() == 1.469 + username.substr(0, aSearchString.length)) 1.470 + { 1.471 + matchingLogins.push(logins[i]); 1.472 + } 1.473 + } 1.474 + log(matchingLogins.length, "autocomplete logins avail."); 1.475 + result = new UserAutoCompleteResult(aSearchString, matchingLogins); 1.476 + } 1.477 + 1.478 + return result; 1.479 + }, 1.480 + 1.481 + 1.482 + 1.483 + 1.484 + /* ------- Internal methods / callbacks for document integration ------- */ 1.485 + 1.486 + 1.487 + 1.488 + 1.489 + /* 1.490 + * _getPasswordOrigin 1.491 + * 1.492 + * Get the parts of the URL we want for identification. 1.493 + */ 1.494 + _getPasswordOrigin : function (uriString, allowJS) { 1.495 + var realm = ""; 1.496 + try { 1.497 + var uri = Services.io.newURI(uriString, null, null); 1.498 + 1.499 + if (allowJS && uri.scheme == "javascript") 1.500 + return "javascript:" 1.501 + 1.502 + realm = uri.scheme + "://" + uri.host; 1.503 + 1.504 + // If the URI explicitly specified a port, only include it when 1.505 + // it's not the default. (We never want "http://foo.com:80") 1.506 + var port = uri.port; 1.507 + if (port != -1) { 1.508 + var handler = Services.io.getProtocolHandler(uri.scheme); 1.509 + if (port != handler.defaultPort) 1.510 + realm += ":" + port; 1.511 + } 1.512 + 1.513 + } catch (e) { 1.514 + // bug 159484 - disallow url types that don't support a hostPort. 1.515 + // (although we handle "javascript:..." as a special case above.) 1.516 + log("Couldn't parse origin for", uriString); 1.517 + realm = null; 1.518 + } 1.519 + 1.520 + return realm; 1.521 + }, 1.522 + 1.523 + _getActionOrigin : function (form) { 1.524 + var uriString = form.action; 1.525 + 1.526 + // A blank or missing action submits to where it came from. 1.527 + if (uriString == "") 1.528 + uriString = form.baseURI; // ala bug 297761 1.529 + 1.530 + return this._getPasswordOrigin(uriString, true); 1.531 + }, 1.532 + 1.533 + 1.534 + /* 1.535 + * fillForm 1.536 + * 1.537 + * Fill the form with login information if we can find it. 1.538 + */ 1.539 + fillForm : function (form) { 1.540 + log("fillForm processing form[ id:", form.id, "]"); 1.541 + return LoginManagerContent._fillForm(form, true, true, false, null)[0]; 1.542 + }, 1.543 + 1.544 +}; // end of LoginManager implementation 1.545 + 1.546 + 1.547 + 1.548 + 1.549 +// nsIAutoCompleteResult implementation 1.550 +function UserAutoCompleteResult (aSearchString, matchingLogins) { 1.551 + function loginSort(a,b) { 1.552 + var userA = a.username.toLowerCase(); 1.553 + var userB = b.username.toLowerCase(); 1.554 + 1.555 + if (userA < userB) 1.556 + return -1; 1.557 + 1.558 + if (userB > userA) 1.559 + return 1; 1.560 + 1.561 + return 0; 1.562 + }; 1.563 + 1.564 + this.searchString = aSearchString; 1.565 + this.logins = matchingLogins.sort(loginSort); 1.566 + this.matchCount = matchingLogins.length; 1.567 + 1.568 + if (this.matchCount > 0) { 1.569 + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; 1.570 + this.defaultIndex = 0; 1.571 + } 1.572 +} 1.573 + 1.574 +UserAutoCompleteResult.prototype = { 1.575 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, 1.576 + Ci.nsISupportsWeakReference]), 1.577 + 1.578 + // private 1.579 + logins : null, 1.580 + 1.581 + // Allow autoCompleteSearch to get at the JS object so it can 1.582 + // modify some readonly properties for internal use. 1.583 + get wrappedJSObject() { 1.584 + return this; 1.585 + }, 1.586 + 1.587 + // Interfaces from idl... 1.588 + searchString : null, 1.589 + searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 1.590 + defaultIndex : -1, 1.591 + errorDescription : "", 1.592 + matchCount : 0, 1.593 + 1.594 + getValueAt : function (index) { 1.595 + if (index < 0 || index >= this.logins.length) 1.596 + throw "Index out of range."; 1.597 + 1.598 + return this.logins[index].username; 1.599 + }, 1.600 + 1.601 + getLabelAt: function(index) { 1.602 + return this.getValueAt(index); 1.603 + }, 1.604 + 1.605 + getCommentAt : function (index) { 1.606 + return ""; 1.607 + }, 1.608 + 1.609 + getStyleAt : function (index) { 1.610 + return ""; 1.611 + }, 1.612 + 1.613 + getImageAt : function (index) { 1.614 + return ""; 1.615 + }, 1.616 + 1.617 + getFinalCompleteValueAt : function (index) { 1.618 + return this.getValueAt(index); 1.619 + }, 1.620 + 1.621 + removeValueAt : function (index, removeFromDB) { 1.622 + if (index < 0 || index >= this.logins.length) 1.623 + throw "Index out of range."; 1.624 + 1.625 + var [removedLogin] = this.logins.splice(index, 1); 1.626 + 1.627 + this.matchCount--; 1.628 + if (this.defaultIndex > this.logins.length) 1.629 + this.defaultIndex--; 1.630 + 1.631 + if (removeFromDB) { 1.632 + var pwmgr = Cc["@mozilla.org/login-manager;1"]. 1.633 + getService(Ci.nsILoginManager); 1.634 + pwmgr.removeLogin(removedLogin); 1.635 + } 1.636 + } 1.637 +}; 1.638 + 1.639 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);