summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/nsLoginManager.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/nsLoginManager.js')
-rw-r--r--toolkit/components/passwordmgr/nsLoginManager.js541
1 files changed, 541 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/nsLoginManager.js b/toolkit/components/passwordmgr/nsLoginManager.js
new file mode 100644
index 000000000..514351fa5
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -0,0 +1,541 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/LoginManagerContent.jsm"); /* global UserAutoCompleteResult */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("nsLoginManager");
+ return logger;
+});
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+function LoginManager() {
+ this.init();
+}
+
+LoginManager.prototype = {
+
+ classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManager,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIInterfaceRequestor]),
+ getInterface(aIID) {
+ if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
+ let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
+ return ir.getInterface(aIID);
+ }
+
+ if (aIID.equals(Ci.nsIVariant)) {
+ // Allows unwrapping the JavaScript object for regression tests.
+ return this;
+ }
+
+ throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+
+ /* ---------- private members ---------- */
+
+
+ __formFillService: null, // FormFillController, for username autocompleting
+ get _formFillService() {
+ if (!this.__formFillService) {
+ this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ }
+ return this.__formFillService;
+ },
+
+
+ _storage: null, // Storage component which contains the saved logins
+ _prefBranch: null, // Preferences service
+ _remember: true, // mirrors signon.rememberSignons preference
+
+
+ /**
+ * Initialize the Login Manager. Automatically called when service
+ * is created.
+ *
+ * Note: Service created in /browser/base/content/browser.js,
+ * delayedStartup()
+ */
+ init() {
+
+ // Cache references to current |this| in utility objects
+ this._observer._pwmgr = this;
+
+ // Preferences. Add observer so we get notified of changes.
+ this._prefBranch = Services.prefs.getBranch("signon.");
+ this._prefBranch.addObserver("rememberSignons", this._observer, false);
+
+ this._remember = this._prefBranch.getBoolPref("rememberSignons");
+ this._autoCompleteLookupPromise = null;
+
+ // Form submit observer checks forms for new logins and pw changes.
+ Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
+
+ if (Services.appinfo.processType ===
+ Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.obs.addObserver(this._observer, "passwordmgr-storage-replace",
+ false);
+
+ // Initialize storage so that asynchronous data loading can start.
+ this._initStorage();
+ }
+
+ Services.obs.addObserver(this._observer, "gather-telemetry", false);
+ },
+
+
+ _initStorage() {
+ let contractID;
+ if (AppConstants.platform == "android") {
+ contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+ } else {
+ contractID = "@mozilla.org/login-manager/storage/json;1";
+ }
+ try {
+ let catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ contractID = catMan.getCategoryEntry("login-manager-storage",
+ "nsILoginManagerStorage");
+ log.debug("Found alternate nsILoginManagerStorage with contract ID:", contractID);
+ } catch (e) {
+ log.debug("No alternate nsILoginManagerStorage registered");
+ }
+
+ this._storage = Cc[contractID].
+ createInstance(Ci.nsILoginManagerStorage);
+ this.initializationPromise = this._storage.initialize();
+ },
+
+
+ /* ---------- Utility objects ---------- */
+
+
+ /**
+ * Internal utility object, implements the nsIObserver interface.
+ * Used to receive notification for: form submission, preference changes.
+ */
+ _observer: {
+ _pwmgr: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ var prefName = data;
+ log.debug("got change to", prefName, "preference");
+
+ if (prefName == "rememberSignons") {
+ this._pwmgr._remember =
+ this._pwmgr._prefBranch.getBoolPref("rememberSignons");
+ } else {
+ log.debug("Oops! Pref not handled, change ignored.");
+ }
+ } else if (topic == "xpcom-shutdown") {
+ delete this._pwmgr.__formFillService;
+ delete this._pwmgr._storage;
+ delete this._pwmgr._prefBranch;
+ this._pwmgr = null;
+ } else if (topic == "passwordmgr-storage-replace") {
+ Task.spawn(function* () {
+ yield this._pwmgr._storage.terminate();
+ this._pwmgr._initStorage();
+ yield this._pwmgr.initializationPromise;
+ Services.obs.notifyObservers(null,
+ "passwordmgr-storage-replace-complete", null);
+ }.bind(this));
+ } else if (topic == "gather-telemetry") {
+ // When testing, the "data" parameter is a string containing the
+ // reference time in milliseconds for time-based statistics.
+ this._pwmgr._gatherTelemetry(data ? parseInt(data)
+ : new Date().getTime());
+ } else {
+ log.debug("Oops! Unexpected notification:", topic);
+ }
+ }
+ },
+
+ /**
+ * Collects statistics about the current logins and settings. The telemetry
+ * histograms used here are not accumulated, but are reset each time this
+ * function is called, since it can be called multiple times in a session.
+ *
+ * This function might also not be called at all in the current session.
+ *
+ * @param referenceTimeMs
+ * Current time used to calculate time-based statistics, expressed as
+ * the number of milliseconds since January 1, 1970, 00:00:00 UTC.
+ * This is set to a fake value during unit testing.
+ */
+ _gatherTelemetry(referenceTimeMs) {
+ function clearAndGetHistogram(histogramId) {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.clear();
+ return histogram;
+ }
+
+ clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add(
+ this.getAllDisabledHosts({}).length
+ );
+ clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add(
+ this.countLogins("", "", "")
+ );
+ clearAndGetHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS").add(
+ this.countLogins("", null, "")
+ );
+
+ // This is a boolean histogram, and not a flag, because we don't want to
+ // record any value if _gatherTelemetry is not called.
+ clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(this._remember);
+
+ // Don't try to get logins if MP is enabled, since we don't want to show a MP prompt.
+ if (!this.isLoggedIn) {
+ return;
+ }
+
+ let logins = this.getAllLogins({});
+
+ let usernamePresentHistogram = clearAndGetHistogram("PWMGR_USERNAME_PRESENT");
+ let loginLastUsedDaysHistogram = clearAndGetHistogram("PWMGR_LOGIN_LAST_USED_DAYS");
+
+ let hostnameCount = new Map();
+ for (let login of logins) {
+ usernamePresentHistogram.add(!!login.username);
+
+ let hostname = login.hostname;
+ hostnameCount.set(hostname, (hostnameCount.get(hostname) || 0 ) + 1);
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed;
+ if (timeLastUsedAgeMs > 0) {
+ loginLastUsedDaysHistogram.add(
+ Math.floor(timeLastUsedAgeMs / MS_PER_DAY)
+ );
+ }
+ }
+
+ let passwordsCountHistogram = clearAndGetHistogram("PWMGR_NUM_PASSWORDS_PER_HOSTNAME");
+ for (let count of hostnameCount.values()) {
+ passwordsCountHistogram.add(count);
+ }
+ },
+
+
+
+
+
+ /* ---------- Primary Public interfaces ---------- */
+
+
+
+
+ /**
+ * @type Promise
+ * This promise is resolved when initialization is complete, and is rejected
+ * in case the asynchronous part of initialization failed.
+ */
+ initializationPromise: null,
+
+
+ /**
+ * Add a new login to login storage.
+ */
+ addLogin(login) {
+ // Sanity check the login
+ if (login.hostname == null || login.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (login.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (login.password == null || login.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (login.formSubmitURL || login.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (login.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (login.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (login.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+
+ // Look for an existing entry.
+ var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
+ login.httpRealm);
+
+ if (logins.some(l => login.matches(l, true))) {
+ throw new Error("This login already exists.");
+ }
+
+ log.debug("Adding login");
+ return this._storage.addLogin(login);
+ },
+
+ /**
+ * Remove the specified login from the stored logins.
+ */
+ removeLogin(login) {
+ log.debug("Removing login");
+ return this._storage.removeLogin(login);
+ },
+
+
+ /**
+ * Change the specified login to match the new login.
+ */
+ modifyLogin(oldLogin, newLogin) {
+ log.debug("Modifying login");
+ return this._storage.modifyLogin(oldLogin, newLogin);
+ },
+
+
+ /**
+ * Get a dump of all stored logins. Used by the login manager UI.
+ *
+ * @param count - only needed for XPCOM.
+ * @return {nsILoginInfo[]} - If there are no logins, the array is empty.
+ */
+ getAllLogins(count) {
+ log.debug("Getting a list of all logins");
+ return this._storage.getAllLogins(count);
+ },
+
+
+ /**
+ * Remove all stored logins.
+ */
+ removeAllLogins() {
+ log.debug("Removing all logins");
+ this._storage.removeAllLogins();
+ },
+
+ /**
+ * Get a list of all origins for which logins are disabled.
+ *
+ * @param {Number} count - only needed for XPCOM.
+ *
+ * @return {String[]} of disabled origins. If there are no disabled origins,
+ * the array is empty.
+ */
+ getAllDisabledHosts(count) {
+ log.debug("Getting a list of all disabled origins");
+
+ let disabledHosts = [];
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext();
+ if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) {
+ disabledHosts.push(perm.principal.URI.prePath);
+ }
+ }
+
+ if (count)
+ count.value = disabledHosts.length; // needed for XPCOM
+
+ log.debug("getAllDisabledHosts: returning", disabledHosts.length, "disabled hosts.");
+ return disabledHosts;
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria.
+ */
+ findLogins(count, origin, formActionOrigin, httpRealm) {
+ log.debug("Searching for logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.findLogins(count, origin, formActionOrigin,
+ httpRealm);
+ },
+
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ log.debug("Searching for logins");
+
+ matchData.QueryInterface(Ci.nsIPropertyBag2);
+ if (!matchData.hasKey("guid")) {
+ if (!matchData.hasKey("hostname")) {
+ log.warn("searchLogins: A `hostname` is recommended");
+ }
+
+ if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) {
+ log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended");
+ }
+ }
+
+ return this._storage.searchLogins(count, matchData);
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria,
+ * returns only the count.
+ */
+ countLogins(origin, formActionOrigin, httpRealm) {
+ log.debug("Counting logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.countLogins(origin, formActionOrigin, httpRealm);
+ },
+
+
+ get uiBusy() {
+ return this._storage.uiBusy;
+ },
+
+
+ get isLoggedIn() {
+ return this._storage.isLoggedIn;
+ },
+
+
+ /**
+ * Check to see if user has disabled saving logins for the origin.
+ */
+ getLoginSavingEnabled(origin) {
+ log.debug("Checking if logins to", origin, "can be saved.");
+ if (!this._remember) {
+ return false;
+ }
+
+ let uri = Services.io.newURI(origin, null, null);
+ return Services.perms.testPermission(uri, PERMISSION_SAVE_LOGINS) != Services.perms.DENY_ACTION;
+ },
+
+
+ /**
+ * Enable or disable storing logins for the specified origin.
+ */
+ setLoginSavingEnabled(origin, enabled) {
+ // Throws if there are bogus values.
+ LoginHelper.checkHostnameValue(origin);
+
+ let uri = Services.io.newURI(origin, null, null);
+ if (enabled) {
+ Services.perms.remove(uri, PERMISSION_SAVE_LOGINS);
+ } else {
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ }
+
+ log.debug("Login saving for", origin, "now enabled?", enabled);
+ LoginHelper.notifyStorageChanged(enabled ? "hostSavingEnabled" : "hostSavingDisabled", origin);
+ },
+
+ /**
+ * Yuck. This is called directly by satchel:
+ * nsFormFillController::StartSearch()
+ * [toolkit/components/satchel/nsFormFillController.cpp]
+ *
+ * We really ought to have a simple way for code to register an
+ * auto-complete provider, and not have satchel calling pwmgr directly.
+ */
+ autoCompleteSearchAsync(aSearchString, aPreviousResult,
+ aElement, aCallback) {
+ // aPreviousResult is an nsIAutoCompleteResult, aElement is
+ // nsIDOMHTMLInputElement
+
+ let form = LoginFormFactory.createFromField(aElement);
+ let isSecure = InsecurePasswordUtils.isFormSecure(form);
+ let isPasswordField = aElement.type == "password";
+
+ let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => {
+ // If the search was canceled before we got our
+ // results, don't bother reporting them.
+ if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
+ return;
+ }
+
+ this._autoCompleteLookupPromise = null;
+ let results = new UserAutoCompleteResult(aSearchString, logins, {
+ messageManager,
+ isSecure,
+ isPasswordField,
+ });
+ aCallback.onSearchCompletion(results);
+ };
+
+ if (isPasswordField && aSearchString) {
+ // Return empty result on password fields with password already filled.
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ if (!this._remember) {
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
+
+ let previousResult;
+ if (aPreviousResult) {
+ previousResult = { searchString: aPreviousResult.searchString,
+ logins: aPreviousResult.wrappedJSObject.logins };
+ } else {
+ previousResult = null;
+ }
+
+ let rect = BrowserUtils.getElementBoundingScreenRect(aElement);
+ let acLookupPromise = this._autoCompleteLookupPromise =
+ LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult,
+ aElement, rect);
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise))
+ .then(null, Cu.reportError);
+ },
+
+ stopSearch() {
+ this._autoCompleteLookupPromise = null;
+ },
+}; // end of LoginManager implementation
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);