summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/ClientID.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/ClientID.jsm')
-rw-r--r--toolkit/modules/ClientID.jsm231
1 files changed, 231 insertions, 0 deletions
diff --git a/toolkit/modules/ClientID.jsm b/toolkit/modules/ClientID.jsm
new file mode 100644
index 000000000..e29e1ee30
--- /dev/null
+++ b/toolkit/modules/ClientID.jsm
@@ -0,0 +1,231 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["ClientID"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "ClientID::";
+
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+ "resource://services-common/utils.js");
+
+XPCOMUtils.defineLazyGetter(this, "gDatareportingPath", () => {
+ return OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStateFilePath", () => {
+ return OS.Path.join(gDatareportingPath, "state.json");
+});
+
+const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+/**
+ * Checks if client ID has a valid format.
+ *
+ * @param {String} id A string containing the client ID.
+ * @return {Boolean} True when the client ID has valid format, or False
+ * otherwise.
+ */
+function isValidClientID(id) {
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ return UUID_REGEX.test(id);
+}
+
+this.ClientID = Object.freeze({
+ /**
+ * This returns a promise resolving to the the stable client ID we use for
+ * data reporting (FHR & Telemetry). Previously exising FHR client IDs are
+ * migrated to this.
+ *
+ * WARNING: This functionality is duplicated for Android (see GeckoProfile.getClientId
+ * for more). There are Java tests (TestGeckoProfile) to ensure the functionality is
+ * consistent and Gecko tests to come (bug 1249156). However, THIS IS NOT FOOLPROOF.
+ * Be careful when changing this code and, in particular, the underlying file format.
+ *
+ * @return {Promise<string>} The stable client ID.
+ */
+ getClientID: function() {
+ return ClientIDImpl.getClientID();
+ },
+
+/**
+ * Get the client id synchronously without hitting the disk.
+ * This returns:
+ * - the current on-disk client id if it was already loaded
+ * - the client id that we cached into preferences (if any)
+ * - null otherwise
+ */
+ getCachedClientID: function() {
+ return ClientIDImpl.getCachedClientID();
+ },
+
+ /**
+ * Only used for testing. Invalidates the client ID so that it gets read
+ * again from file.
+ */
+ _reset: function() {
+ return ClientIDImpl._reset();
+ },
+});
+
+var ClientIDImpl = {
+ _clientID: null,
+ _loadClientIdTask: null,
+ _saveClientIdTask: null,
+ _logger: null,
+
+ _loadClientID: function () {
+ if (this._loadClientIdTask) {
+ return this._loadClientIdTask;
+ }
+
+ this._loadClientIdTask = this._doLoadClientID();
+ let clear = () => this._loadClientIdTask = null;
+ this._loadClientIdTask.then(clear, clear);
+ return this._loadClientIdTask;
+ },
+
+ _doLoadClientID: Task.async(function* () {
+ // As we want to correlate FHR and telemetry data (and move towards unifying the two),
+ // we first moved the ID management from the FHR implementation to the datareporting
+ // service, then to a common shared module.
+ // Consequently, we try to import an existing FHR ID, so we can keep using it.
+
+ // Try to load the client id from the DRS state file first.
+ try {
+ let state = yield CommonUtils.readJSON(gStateFilePath);
+ if (state && this.updateClientID(state.clientID)) {
+ return this._clientID;
+ }
+ } catch (e) {
+ // fall through to next option
+ }
+
+ // If we dont have DRS state yet, try to import from the FHR state.
+ try {
+ let fhrStatePath = OS.Path.join(OS.Constants.Path.profileDir, "healthreport", "state.json");
+ let state = yield CommonUtils.readJSON(fhrStatePath);
+ if (state && this.updateClientID(state.clientID)) {
+ this._saveClientID();
+ return this._clientID;
+ }
+ } catch (e) {
+ // fall through to next option
+ }
+
+ // We dont have an id from FHR yet, generate a new ID.
+ this.updateClientID(CommonUtils.generateUUID());
+ this._saveClientIdTask = this._saveClientID();
+
+ // Wait on persisting the id. Otherwise failure to save the ID would result in
+ // the client creating and subsequently sending multiple IDs to the server.
+ // This would appear as multiple clients submitting similar data, which would
+ // result in orphaning.
+ yield this._saveClientIdTask;
+
+ return this._clientID;
+ }),
+
+ /**
+ * Save the client ID to the client ID file.
+ *
+ * @return {Promise} A promise resolved when the client ID is saved to disk.
+ */
+ _saveClientID: Task.async(function* () {
+ let obj = { clientID: this._clientID };
+ yield OS.File.makeDir(gDatareportingPath);
+ yield CommonUtils.writeJSON(obj, gStateFilePath);
+ this._saveClientIdTask = null;
+ }),
+
+ /**
+ * This returns a promise resolving to the the stable client ID we use for
+ * data reporting (FHR & Telemetry). Previously exising FHR client IDs are
+ * migrated to this.
+ *
+ * @return {Promise<string>} The stable client ID.
+ */
+ getClientID: function() {
+ if (!this._clientID) {
+ return this._loadClientID();
+ }
+
+ return Promise.resolve(this._clientID);
+ },
+
+ /**
+ * Get the client id synchronously without hitting the disk.
+ * This returns:
+ * - the current on-disk client id if it was already loaded
+ * - the client id that we cached into preferences (if any)
+ * - null otherwise
+ */
+ getCachedClientID: function() {
+ if (this._clientID) {
+ // Already loaded the client id from disk.
+ return this._clientID;
+ }
+
+ // Not yet loaded, return the cached client id if we have one.
+ let id = Preferences.get(PREF_CACHED_CLIENTID, null);
+ if (id === null) {
+ return null;
+ }
+ if (!isValidClientID(id)) {
+ this._log.error("getCachedClientID - invalid client id in preferences, resetting", id);
+ Preferences.reset(PREF_CACHED_CLIENTID);
+ return null;
+ }
+ return id;
+ },
+
+ /*
+ * Resets the provider. This is for testing only.
+ */
+ _reset: Task.async(function* () {
+ yield this._loadClientIdTask;
+ yield this._saveClientIdTask;
+ this._clientID = null;
+ }),
+
+ /**
+ * Sets the client id to the given value and updates the value cached in
+ * preferences only if the given id is a valid.
+ *
+ * @param {String} id A string containing the client ID.
+ * @return {Boolean} True when the client ID has valid format, or False
+ * otherwise.
+ */
+ updateClientID: function (id) {
+ if (!isValidClientID(id)) {
+ this._log.error("updateClientID - invalid client ID", id);
+ return false;
+ }
+
+ this._clientID = id;
+ Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
+ return true;
+ },
+
+ /**
+ * A helper for getting access to telemetry logger.
+ */
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+};