diff options
Diffstat (limited to 'toolkit/modules/ClientID.jsm')
-rw-r--r-- | toolkit/modules/ClientID.jsm | 231 |
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; + }, +}; |