/* 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; }, };