michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, michael@0: "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); michael@0: michael@0: var debug = false; michael@0: function log(...pieces) { michael@0: function generateLogMessage(args) { michael@0: let strings = ['Login Manager:']; michael@0: michael@0: args.forEach(function(arg) { michael@0: if (typeof arg === 'string') { michael@0: strings.push(arg); michael@0: } else if (typeof arg === 'undefined') { michael@0: strings.push('undefined'); michael@0: } else if (arg === null) { michael@0: strings.push('null'); michael@0: } else { michael@0: try { michael@0: strings.push(JSON.stringify(arg, null, 2)); michael@0: } catch(err) { michael@0: strings.push("<>"); michael@0: } michael@0: } michael@0: }); michael@0: return strings.join(' '); michael@0: } michael@0: michael@0: if (!debug) michael@0: return; michael@0: michael@0: let message = generateLogMessage(pieces); michael@0: dump(message + "\n"); michael@0: Services.console.logStringMessage(message); michael@0: } michael@0: michael@0: function LoginManager() { michael@0: this.init(); michael@0: } michael@0: michael@0: LoginManager.prototype = { michael@0: michael@0: classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, michael@0: Ci.nsISupportsWeakReference, michael@0: Ci.nsIInterfaceRequestor]), michael@0: getInterface : function(aIID) { michael@0: if (aIID.equals(Ci.mozIStorageConnection) && this._storage) { michael@0: let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor); michael@0: return ir.getInterface(aIID); michael@0: } michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: /* ---------- private memebers ---------- */ michael@0: michael@0: michael@0: __formFillService : null, // FormFillController, for username autocompleting michael@0: get _formFillService() { michael@0: if (!this.__formFillService) michael@0: this.__formFillService = michael@0: Cc["@mozilla.org/satchel/form-fill-controller;1"]. michael@0: getService(Ci.nsIFormFillController); michael@0: return this.__formFillService; michael@0: }, michael@0: michael@0: michael@0: __storage : null, // Storage component which contains the saved logins michael@0: get _storage() { michael@0: if (!this.__storage) { michael@0: michael@0: var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; michael@0: try { michael@0: var catMan = Cc["@mozilla.org/categorymanager;1"]. michael@0: getService(Ci.nsICategoryManager); michael@0: contractID = catMan.getCategoryEntry("login-manager-storage", michael@0: "nsILoginManagerStorage"); michael@0: log("Found alternate nsILoginManagerStorage with contract ID:", contractID); michael@0: } catch (e) { michael@0: log("No alternate nsILoginManagerStorage registered"); michael@0: } michael@0: michael@0: this.__storage = Cc[contractID]. michael@0: createInstance(Ci.nsILoginManagerStorage); michael@0: try { michael@0: this.__storage.init(); michael@0: } catch (e) { michael@0: log("Initialization of storage component failed:", e); michael@0: this.__storage = null; michael@0: } michael@0: } michael@0: michael@0: return this.__storage; michael@0: }, michael@0: michael@0: _prefBranch : null, // Preferences service michael@0: _remember : true, // mirrors signon.rememberSignons preference michael@0: michael@0: michael@0: /* michael@0: * init michael@0: * michael@0: * Initialize the Login Manager. Automatically called when service michael@0: * is created. michael@0: * michael@0: * Note: Service created in /browser/base/content/browser.js, michael@0: * delayedStartup() michael@0: */ michael@0: init : function () { michael@0: michael@0: // Cache references to current |this| in utility objects michael@0: this._observer._pwmgr = this; michael@0: michael@0: // Preferences. Add observer so we get notified of changes. michael@0: this._prefBranch = Services.prefs.getBranch("signon."); michael@0: this._prefBranch.addObserver("", this._observer, false); michael@0: michael@0: // Get current preference values. michael@0: debug = this._prefBranch.getBoolPref("debug"); michael@0: michael@0: this._remember = this._prefBranch.getBoolPref("rememberSignons"); michael@0: michael@0: // Form submit observer checks forms for new logins and pw changes. michael@0: Services.obs.addObserver(this._observer, "xpcom-shutdown", false); michael@0: michael@0: // XXX gross hacky workaround for bug 881996. The WPL does nothing. michael@0: var progress = Cc["@mozilla.org/docloaderservice;1"]. michael@0: getService(Ci.nsIWebProgress); michael@0: progress.addProgressListener(this._webProgressListener, michael@0: Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); michael@0: }, michael@0: michael@0: michael@0: /* ---------- Utility objects ---------- */ michael@0: michael@0: michael@0: /* michael@0: * _observer object michael@0: * michael@0: * Internal utility object, implements the nsIObserver interface. michael@0: * Used to receive notification for: form submission, preference changes. michael@0: */ michael@0: _observer : { michael@0: _pwmgr : null, michael@0: michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: // nsObserver michael@0: observe : function (subject, topic, data) { michael@0: michael@0: if (topic == "nsPref:changed") { michael@0: var prefName = data; michael@0: log("got change to", prefName, "preference"); michael@0: michael@0: if (prefName == "debug") { michael@0: debug = this._pwmgr._prefBranch.getBoolPref("debug"); michael@0: } else if (prefName == "rememberSignons") { michael@0: this._pwmgr._remember = michael@0: this._pwmgr._prefBranch.getBoolPref("rememberSignons"); michael@0: } else { michael@0: log("Oops! Pref not handled, change ignored."); michael@0: } michael@0: } else if (topic == "xpcom-shutdown") { michael@0: for (let i in this._pwmgr) { michael@0: try { michael@0: this._pwmgr[i] = null; michael@0: } catch(ex) {} michael@0: } michael@0: this._pwmgr = null; michael@0: } else { michael@0: log("Oops! Unexpected notification:", topic); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: _webProgressListener : { michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, michael@0: Ci.nsISupportsWeakReference]), michael@0: onStateChange : function() { /* NOP */ }, michael@0: onProgressChange : function() { throw "Unexpected onProgressChange"; }, michael@0: onLocationChange : function() { throw "Unexpected onLocationChange"; }, michael@0: onStatusChange : function() { throw "Unexpected onStatusChange"; }, michael@0: onSecurityChange : function() { throw "Unexpected onSecurityChange"; } michael@0: }, michael@0: michael@0: michael@0: michael@0: michael@0: /* ---------- Primary Public interfaces ---------- */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * addLogin michael@0: * michael@0: * Add a new login to login storage. michael@0: */ michael@0: addLogin : function (login) { michael@0: // Sanity check the login michael@0: if (login.hostname == null || login.hostname.length == 0) michael@0: throw "Can't add a login with a null or empty hostname."; michael@0: michael@0: // For logins w/o a username, set to "", not null. michael@0: if (login.username == null) michael@0: throw "Can't add a login with a null username."; michael@0: michael@0: if (login.password == null || login.password.length == 0) michael@0: throw "Can't add a login with a null or empty password."; michael@0: michael@0: if (login.formSubmitURL || login.formSubmitURL == "") { michael@0: // We have a form submit URL. Can't have a HTTP realm. michael@0: if (login.httpRealm != null) michael@0: throw "Can't add a login with both a httpRealm and formSubmitURL."; michael@0: } else if (login.httpRealm) { michael@0: // We have a HTTP realm. Can't have a form submit URL. michael@0: if (login.formSubmitURL != null) michael@0: throw "Can't add a login with both a httpRealm and formSubmitURL."; michael@0: } else { michael@0: // Need one or the other! michael@0: throw "Can't add a login without a httpRealm or formSubmitURL."; michael@0: } michael@0: michael@0: michael@0: // Look for an existing entry. michael@0: var logins = this.findLogins({}, login.hostname, login.formSubmitURL, michael@0: login.httpRealm); michael@0: michael@0: if (logins.some(function(l) login.matches(l, true))) michael@0: throw "This login already exists."; michael@0: michael@0: log("Adding login"); michael@0: return this._storage.addLogin(login); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * removeLogin michael@0: * michael@0: * Remove the specified login from the stored logins. michael@0: */ michael@0: removeLogin : function (login) { michael@0: log("Removing login"); michael@0: return this._storage.removeLogin(login); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * modifyLogin michael@0: * michael@0: * Change the specified login to match the new login. michael@0: */ michael@0: modifyLogin : function (oldLogin, newLogin) { michael@0: log("Modifying login"); michael@0: return this._storage.modifyLogin(oldLogin, newLogin); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getAllLogins michael@0: * michael@0: * Get a dump of all stored logins. Used by the login manager UI. michael@0: * michael@0: * |count| is only needed for XPCOM. michael@0: * michael@0: * Returns an array of logins. If there are no logins, the array is empty. michael@0: */ michael@0: getAllLogins : function (count) { michael@0: log("Getting a list of all logins"); michael@0: return this._storage.getAllLogins(count); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * removeAllLogins michael@0: * michael@0: * Remove all stored logins. michael@0: */ michael@0: removeAllLogins : function () { michael@0: log("Removing all logins"); michael@0: this._storage.removeAllLogins(); michael@0: }, michael@0: michael@0: /* michael@0: * getAllDisabledHosts michael@0: * michael@0: * Get a list of all hosts for which logins are disabled. michael@0: * michael@0: * |count| is only needed for XPCOM. michael@0: * michael@0: * Returns an array of disabled logins. If there are no disabled logins, michael@0: * the array is empty. michael@0: */ michael@0: getAllDisabledHosts : function (count) { michael@0: log("Getting a list of all disabled hosts"); michael@0: return this._storage.getAllDisabledHosts(count); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * findLogins michael@0: * michael@0: * Search for the known logins for entries matching the specified criteria. michael@0: */ michael@0: findLogins : function (count, hostname, formSubmitURL, httpRealm) { michael@0: log("Searching for logins matching host:", hostname, michael@0: "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm); michael@0: michael@0: return this._storage.findLogins(count, hostname, formSubmitURL, michael@0: httpRealm); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * searchLogins michael@0: * michael@0: * Public wrapper around _searchLogins to convert the nsIPropertyBag to a michael@0: * JavaScript object and decrypt the results. michael@0: * michael@0: * Returns an array of decrypted nsILoginInfo. michael@0: */ michael@0: searchLogins : function(count, matchData) { michael@0: log("Searching for logins"); michael@0: michael@0: return this._storage.searchLogins(count, matchData); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * countLogins michael@0: * michael@0: * Search for the known logins for entries matching the specified criteria, michael@0: * returns only the count. michael@0: */ michael@0: countLogins : function (hostname, formSubmitURL, httpRealm) { michael@0: log("Counting logins matching host:", hostname, michael@0: "formSubmitURL:", formSubmitURL, "httpRealm:", httpRealm); michael@0: michael@0: return this._storage.countLogins(hostname, formSubmitURL, httpRealm); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * uiBusy michael@0: */ michael@0: get uiBusy() { michael@0: return this._storage.uiBusy; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * isLoggedIn michael@0: */ michael@0: get isLoggedIn() { michael@0: return this._storage.isLoggedIn; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getLoginSavingEnabled michael@0: * michael@0: * Check to see if user has disabled saving logins for the host. michael@0: */ michael@0: getLoginSavingEnabled : function (host) { michael@0: log("Checking if logins to", host, "can be saved."); michael@0: if (!this._remember) michael@0: return false; michael@0: michael@0: return this._storage.getLoginSavingEnabled(host); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * setLoginSavingEnabled michael@0: * michael@0: * Enable or disable storing logins for the specified host. michael@0: */ michael@0: setLoginSavingEnabled : function (hostname, enabled) { michael@0: // Nulls won't round-trip with getAllDisabledHosts(). michael@0: if (hostname.indexOf("\0") != -1) michael@0: throw "Invalid hostname"; michael@0: michael@0: log("Login saving for", hostname, "now enabled?", enabled); michael@0: return this._storage.setLoginSavingEnabled(hostname, enabled); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * autoCompleteSearch michael@0: * michael@0: * Yuck. This is called directly by satchel: michael@0: * nsFormFillController::StartSearch() michael@0: * [toolkit/components/satchel/src/nsFormFillController.cpp] michael@0: * michael@0: * We really ought to have a simple way for code to register an michael@0: * auto-complete provider, and not have satchel calling pwmgr directly. michael@0: */ michael@0: autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { michael@0: // aPreviousResult & aResult are nsIAutoCompleteResult, michael@0: // aElement is nsIDOMHTMLInputElement michael@0: michael@0: if (!this._remember) michael@0: return null; michael@0: michael@0: log("AutoCompleteSearch invoked. Search is:", aSearchString); michael@0: michael@0: var result = null; michael@0: michael@0: if (aPreviousResult && michael@0: aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) { michael@0: log("Using previous autocomplete result"); michael@0: result = aPreviousResult; michael@0: result.wrappedJSObject.searchString = aSearchString; michael@0: michael@0: // We have a list of results for a shorter search string, so just michael@0: // filter them further based on the new search string. michael@0: // Count backwards, because result.matchCount is decremented michael@0: // when we remove an entry. michael@0: for (var i = result.matchCount - 1; i >= 0; i--) { michael@0: var match = result.getValueAt(i); michael@0: michael@0: // Remove results that are too short, or have different prefix. michael@0: if (aSearchString.length > match.length || michael@0: aSearchString.toLowerCase() != michael@0: match.substr(0, aSearchString.length).toLowerCase()) michael@0: { michael@0: log("Removing autocomplete entry:", match); michael@0: result.removeValueAt(i, false); michael@0: } michael@0: } michael@0: } else { michael@0: log("Creating new autocomplete search result."); michael@0: michael@0: var doc = aElement.ownerDocument; michael@0: var origin = this._getPasswordOrigin(doc.documentURI); michael@0: var actionOrigin = this._getActionOrigin(aElement.form); michael@0: michael@0: // This shouldn't trigger a master password prompt, because we michael@0: // don't attach to the input until after we successfully obtain michael@0: // logins for the form. michael@0: var logins = this.findLogins({}, origin, actionOrigin, null); michael@0: var matchingLogins = []; michael@0: michael@0: // Filter out logins that don't match the search prefix. Also michael@0: // filter logins without a username, since that's confusing to see michael@0: // in the dropdown and we can't autocomplete them anyway. michael@0: for (i = 0; i < logins.length; i++) { michael@0: var username = logins[i].username.toLowerCase(); michael@0: if (username && michael@0: aSearchString.length <= username.length && michael@0: aSearchString.toLowerCase() == michael@0: username.substr(0, aSearchString.length)) michael@0: { michael@0: matchingLogins.push(logins[i]); michael@0: } michael@0: } michael@0: log(matchingLogins.length, "autocomplete logins avail."); michael@0: result = new UserAutoCompleteResult(aSearchString, matchingLogins); michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: michael@0: michael@0: michael@0: /* ------- Internal methods / callbacks for document integration ------- */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * _getPasswordOrigin michael@0: * michael@0: * Get the parts of the URL we want for identification. michael@0: */ michael@0: _getPasswordOrigin : function (uriString, allowJS) { michael@0: var realm = ""; michael@0: try { michael@0: var uri = Services.io.newURI(uriString, null, null); michael@0: michael@0: if (allowJS && uri.scheme == "javascript") michael@0: return "javascript:" michael@0: michael@0: realm = uri.scheme + "://" + uri.host; michael@0: michael@0: // If the URI explicitly specified a port, only include it when michael@0: // it's not the default. (We never want "http://foo.com:80") michael@0: var port = uri.port; michael@0: if (port != -1) { michael@0: var handler = Services.io.getProtocolHandler(uri.scheme); michael@0: if (port != handler.defaultPort) michael@0: realm += ":" + port; michael@0: } michael@0: michael@0: } catch (e) { michael@0: // bug 159484 - disallow url types that don't support a hostPort. michael@0: // (although we handle "javascript:..." as a special case above.) michael@0: log("Couldn't parse origin for", uriString); michael@0: realm = null; michael@0: } michael@0: michael@0: return realm; michael@0: }, michael@0: michael@0: _getActionOrigin : function (form) { michael@0: var uriString = form.action; michael@0: michael@0: // A blank or missing action submits to where it came from. michael@0: if (uriString == "") michael@0: uriString = form.baseURI; // ala bug 297761 michael@0: michael@0: return this._getPasswordOrigin(uriString, true); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * fillForm michael@0: * michael@0: * Fill the form with login information if we can find it. michael@0: */ michael@0: fillForm : function (form) { michael@0: log("fillForm processing form[ id:", form.id, "]"); michael@0: return LoginManagerContent._fillForm(form, true, true, false, null)[0]; michael@0: }, michael@0: michael@0: }; // end of LoginManager implementation michael@0: michael@0: michael@0: michael@0: michael@0: // nsIAutoCompleteResult implementation michael@0: function UserAutoCompleteResult (aSearchString, matchingLogins) { michael@0: function loginSort(a,b) { michael@0: var userA = a.username.toLowerCase(); michael@0: var userB = b.username.toLowerCase(); michael@0: michael@0: if (userA < userB) michael@0: return -1; michael@0: michael@0: if (userB > userA) michael@0: return 1; michael@0: michael@0: return 0; michael@0: }; michael@0: michael@0: this.searchString = aSearchString; michael@0: this.logins = matchingLogins.sort(loginSort); michael@0: this.matchCount = matchingLogins.length; michael@0: michael@0: if (this.matchCount > 0) { michael@0: this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; michael@0: this.defaultIndex = 0; michael@0: } michael@0: } michael@0: michael@0: UserAutoCompleteResult.prototype = { michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: // private michael@0: logins : null, michael@0: michael@0: // Allow autoCompleteSearch to get at the JS object so it can michael@0: // modify some readonly properties for internal use. michael@0: get wrappedJSObject() { michael@0: return this; michael@0: }, michael@0: michael@0: // Interfaces from idl... michael@0: searchString : null, michael@0: searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, michael@0: defaultIndex : -1, michael@0: errorDescription : "", michael@0: matchCount : 0, michael@0: michael@0: getValueAt : function (index) { michael@0: if (index < 0 || index >= this.logins.length) michael@0: throw "Index out of range."; michael@0: michael@0: return this.logins[index].username; michael@0: }, michael@0: michael@0: getLabelAt: function(index) { michael@0: return this.getValueAt(index); michael@0: }, michael@0: michael@0: getCommentAt : function (index) { michael@0: return ""; michael@0: }, michael@0: michael@0: getStyleAt : function (index) { michael@0: return ""; michael@0: }, michael@0: michael@0: getImageAt : function (index) { michael@0: return ""; michael@0: }, michael@0: michael@0: getFinalCompleteValueAt : function (index) { michael@0: return this.getValueAt(index); michael@0: }, michael@0: michael@0: removeValueAt : function (index, removeFromDB) { michael@0: if (index < 0 || index >= this.logins.length) michael@0: throw "Index out of range."; michael@0: michael@0: var [removedLogin] = this.logins.splice(index, 1); michael@0: michael@0: this.matchCount--; michael@0: if (this.defaultIndex > this.logins.length) michael@0: this.defaultIndex--; michael@0: michael@0: if (removeFromDB) { michael@0: var pwmgr = Cc["@mozilla.org/login-manager;1"]. michael@0: getService(Ci.nsILoginManager); michael@0: pwmgr.removeLogin(removedLogin); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);