--- a/toolkit/components/normandy/lib/Addons.jsm
+++ b/toolkit/components/normandy/lib/Addons.jsm
@@ -42,26 +42,16 @@ var Addons = {
const addon = await AddonManager.getAddonByID(addonId);
if (!addon) {
return null;
}
return this.serializeForSandbox(addon);
},
/**
- * Get information about all installed add-ons.
- * @async
- * @returns {Array<SafeAddon>}
- */
- async getAll(addonId) {
- const addons = await AddonManager.getAllAddons();
- return addons.map(this.serializeForSandbox.bind(this));
- },
-
- /**
* Installs an add-on
*
* @param {string} addonUrl
* Url to download the .xpi for the add-on from.
* @param {object} options
* @param {boolean} options.update=false
* If true, will update an existing installed add-on with the same ID.
* @async
--- a/toolkit/components/normandy/lib/ClientEnvironment.jsm
+++ b/toolkit/components/normandy/lib/ClientEnvironment.jsm
@@ -2,211 +2,107 @@
* 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";
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "ShellService", "resource:///modules/ShellService.jsm");
-ChromeUtils.defineModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
-ChromeUtils.defineModuleGetter(this, "TelemetryArchive", "resource://gre/modules/TelemetryArchive.jsm");
-ChromeUtils.defineModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm");
ChromeUtils.defineModuleGetter(this, "NormandyApi", "resource://normandy/lib/NormandyApi.jsm");
ChromeUtils.defineModuleGetter(
+ this,
+ "ClientEnvironmentBase",
+ "resource://gre/modules/components-utils/ClientEnvironment.jsm"
+);
+ChromeUtils.defineModuleGetter(
this,
"PreferenceExperiments",
"resource://normandy/lib/PreferenceExperiments.jsm"
);
-ChromeUtils.defineModuleGetter(this, "Utils", "resource://normandy/lib/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "Addons", "resource://normandy/lib/Addons.jsm");
-ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
var EXPORTED_SYMBOLS = ["ClientEnvironment"];
// Cached API request for client attributes that are determined by the Normandy
// service.
let _classifyRequest = null;
-var ClientEnvironment = {
+
+class ClientEnvironment extends ClientEnvironmentBase {
/**
* Fetches information about the client that is calculated on the server,
* like geolocation and the current time.
*
* The server request is made lazily and is cached for the entire browser
* session.
*/
- async getClientClassification() {
+ static async getClientClassification() {
if (!_classifyRequest) {
_classifyRequest = NormandyApi.classifyClient();
}
return _classifyRequest;
- },
+ }
- clearClassifyCache() {
+ static clearClassifyCache() {
_classifyRequest = null;
- },
+ }
/**
* Test wrapper that mocks the server request for classifying the client.
* @param {Object} data Fake server data to use
* @param {Function} testFunction Test function to execute while mock data is in effect.
*/
- withMockClassify(data, testFunction) {
+ static withMockClassify(data, testFunction) {
return async function inner() {
const oldRequest = _classifyRequest;
_classifyRequest = Promise.resolve(data);
await testFunction();
_classifyRequest = oldRequest;
};
- },
-
- /**
- * Create an object that provides general information about the client application.
- *
- * RecipeRunner.jsm uses this as part of the context for filter expressions,
- * so avoid adding non-getter functions as attributes, as filter expressions
- * cannot execute functions.
- *
- * Also note that, because filter expressions implicitly resolve promises, you
- * can add getter functions that return promises for async data.
- * @return {Object}
- */
- getEnvironment() {
- const environment = {};
-
- XPCOMUtils.defineLazyGetter(environment, "userId", () => {
- let id = Services.prefs.getCharPref("app.normandy.user_id", "");
- if (!id) {
- // generateUUID adds leading and trailing "{" and "}". strip them off.
- id = generateUUID().toString().slice(1, -1);
- Services.prefs.setCharPref("app.normandy.user_id", id);
- }
- return id;
- });
+ }
- XPCOMUtils.defineLazyGetter(environment, "country", () => {
- return ClientEnvironment.getClientClassification()
- .then(classification => classification.country);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "request_time", () => {
- return ClientEnvironment.getClientClassification()
- .then(classification => classification.request_time);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "distribution", () => {
- return Services.prefs.getCharPref("distribution.id", "default");
- });
-
- XPCOMUtils.defineLazyGetter(environment, "telemetry", async function() {
- const pings = await TelemetryArchive.promiseArchivedPingList();
-
- // get most recent ping per type
- const mostRecentPings = {};
- for (const ping of pings) {
- if (ping.type in mostRecentPings) {
- if (mostRecentPings[ping.type].timestampCreated < ping.timestampCreated) {
- mostRecentPings[ping.type] = ping;
- }
- } else {
- mostRecentPings[ping.type] = ping;
- }
- }
+ static get userId() {
+ let id = Services.prefs.getCharPref("app.normandy.user_id", "");
+ if (!id) {
+ // generateUUID adds leading and trailing "{" and "}". strip them off.
+ id = generateUUID().toString().slice(1, -1);
+ Services.prefs.setCharPref("app.normandy.user_id", id);
+ }
+ return id;
+ }
- const telemetry = {};
- for (const key in mostRecentPings) {
- const ping = mostRecentPings[key];
- telemetry[ping.type] = await TelemetryArchive.promiseArchivedPingById(ping.id);
- }
- return telemetry;
- });
-
- XPCOMUtils.defineLazyGetter(environment, "version", () => {
- return AppConstants.MOZ_APP_VERSION_DISPLAY;
- });
-
- XPCOMUtils.defineLazyGetter(environment, "channel", () => {
- return UpdateUtils.getUpdateChannel(false);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "isDefaultBrowser", () => {
- return ShellService.isDefaultBrowser();
- });
-
- XPCOMUtils.defineLazyGetter(environment, "searchEngine", async function() {
- const searchInitialized = await new Promise(resolve => Services.search.init(resolve));
- if (Components.isSuccessCode(searchInitialized)) {
- return Services.search.defaultEngine.identifier;
- }
- return null;
- });
-
- XPCOMUtils.defineLazyGetter(environment, "syncSetup", () => {
- return Services.prefs.prefHasUserValue("services.sync.username");
- });
+ static get country() {
+ return (async () => {
+ const { country } = await ClientEnvironment.getClientClassification();
+ return country;
+ })();
+ }
- XPCOMUtils.defineLazyGetter(environment, "syncDesktopDevices", () => {
- return Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "syncMobileDevices", () => {
- return Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "syncTotalDevices", () => {
- return environment.syncDesktopDevices + environment.syncMobileDevices;
- });
+ static get request_time() {
+ return (async () => {
+ const { request_time } = await ClientEnvironment.getClientClassification();
+ return request_time;
+ })();
+ }
- XPCOMUtils.defineLazyGetter(environment, "plugins", async function() {
- let plugins = await AddonManager.getAddonsByTypes(["plugin"]);
- plugins = plugins.map(plugin => ({
- name: plugin.name,
- description: plugin.description,
- version: plugin.version,
- }));
- return Utils.keyBy(plugins, "name");
- });
-
- XPCOMUtils.defineLazyGetter(environment, "locale", () => {
- if (Services.locale.getAppLocaleAsLangTag) {
- return Services.locale.getAppLocaleAsLangTag();
- }
-
- return Cc["@mozilla.org/chrome/chrome-registry;1"]
- .getService(Ci.nsIXULChromeRegistry)
- .getSelectedLocale("global");
- });
-
- XPCOMUtils.defineLazyGetter(environment, "doNotTrack", () => {
- return Services.prefs.getBoolPref("privacy.donottrackheader.enabled", false);
- });
-
- XPCOMUtils.defineLazyGetter(environment, "experiments", async () => {
+ static get experiments() {
+ return (async () => {
const names = {all: [], active: [], expired: []};
for (const experiment of await PreferenceExperiments.getAll()) {
names.all.push(experiment.name);
if (experiment.expired) {
names.expired.push(experiment.name);
} else {
names.active.push(experiment.name);
}
}
return names;
- });
-
- XPCOMUtils.defineLazyGetter(environment, "addons", async () => {
- const addons = await Addons.getAll();
- return Utils.keyBy(addons, "id");
- });
+ })();
+ }
- XPCOMUtils.defineLazyGetter(environment, "isFirstRun", () => {
- return Services.prefs.getBoolPref("app.normandy.first_run", true);
- });
-
- return environment;
- },
-};
+ static get isFirstRun() {
+ return Services.prefs.getBoolPref("app.normandy.first_run", true);
+ }
+}
--- a/toolkit/components/normandy/lib/NormandyDriver.jsm
+++ b/toolkit/components/normandy/lib/NormandyDriver.jsm
@@ -45,17 +45,17 @@ var NormandyDriver = function(sandboxMan
}
return Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry)
.getSelectedLocale("global");
},
get userId() {
- return ClientEnvironment.getEnvironment().userId;
+ return ClientEnvironment.userId;
},
log(message, level = "debug") {
const levels = ["debug", "info", "warn", "error"];
if (!levels.includes(level)) {
throw new Error(`Invalid log level "${level}"`);
}
actionLog[level](message);
--- a/toolkit/components/normandy/lib/RecipeRunner.jsm
+++ b/toolkit/components/normandy/lib/RecipeRunner.jsm
@@ -46,16 +46,31 @@ const LAZY_CLASSIFY_PREF = `${PREF_PREFI
const PREFS_TO_WATCH = [
RUN_INTERVAL_PREF,
TELEMETRY_ENABLED_PREF,
SHIELD_ENABLED_PREF,
API_URL_PREF,
];
+/**
+ * cacheProxy returns an object Proxy that will memoize properties of the target.
+ */
+function cacheProxy(target) {
+ const cache = new Map();
+ return new Proxy(target, {
+ get(target, prop, receiver) {
+ if (!cache.has(prop)) {
+ cache.set(prop, target[prop]);
+ }
+ return cache.get(prop);
+ }
+ });
+}
+
var RecipeRunner = {
async init() {
this.enabled = null;
this.checkPrefs(); // sets this.enabled
this.watchPrefs();
// Run if enabled immediately on first run, or if dev mode is enabled.
const firstRun = Services.prefs.getBoolPref(FIRST_RUN_PREF, true);
@@ -230,23 +245,23 @@ var RecipeRunner = {
// Close storage connections
await AddonStudies.close();
Uptake.reportRunner(Uptake.RUNNER_SUCCESS);
},
getFilterContext(recipe) {
+ const environment = cacheProxy(ClientEnvironment);
+ environment.recipe = {
+ id: recipe.id,
+ arguments: recipe.arguments,
+ };
return {
- normandy: Object.assign(ClientEnvironment.getEnvironment(), {
- recipe: {
- id: recipe.id,
- arguments: recipe.arguments,
- },
- }),
+ normandy: environment
};
},
/**
* Evaluate a recipe's filter expression against the environment.
* @param {object} recipe
* @param {string} recipe.filter The expression to evaluate against the environment.
* @return {boolean} The result of evaluating the filter, cast to a bool, or false
deleted file mode 100644
--- a/toolkit/components/normandy/lib/Utils.jsm
+++ /dev/null
@@ -1,36 +0,0 @@
-/* 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";
-
-ChromeUtils.import("resource://normandy/lib/LogManager.jsm");
-
-var EXPORTED_SYMBOLS = ["Utils"];
-
-const log = LogManager.getLogger("utils");
-
-var Utils = {
- /**
- * Convert an array of objects to an object. Each item is keyed by the value
- * of the given key on the item.
- *
- * > list = [{foo: "bar"}, {foo: "baz"}]
- * > keyBy(list, "foo") == {bar: {foo: "bar"}, baz: {foo: "baz"}}
- *
- * @param {Array} list
- * @param {String} key
- * @return {Object}
- */
- keyBy(list, key) {
- return list.reduce((map, item) => {
- if (!(key in item)) {
- log.warn(`Skipping list due to missing key "${key}".`);
- return map;
- }
-
- map[item[key]] = item;
- return map;
- }, {});
- },
-};
--- a/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js
+++ b/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js
@@ -10,99 +10,84 @@ ChromeUtils.import("resource://normandy/
add_task(async function testTelemetry() {
// setup
await SpecialPowers.pushPrefEnv({set: [["privacy.reduceTimerPrecision", true]]});
await TelemetryController.submitExternalPing("testfoo", {foo: 1});
await TelemetryController.submitExternalPing("testbar", {bar: 2});
await TelemetryController.submitExternalPing("testfoo", {foo: 3});
- const environment = ClientEnvironment.getEnvironment();
// Test it can access telemetry
- const telemetry = await environment.telemetry;
+ const telemetry = await ClientEnvironment.telemetry;
is(typeof telemetry, "object", "Telemetry is accesible");
// Test it reads different types of telemetry
is(telemetry.testfoo.payload.foo, 3, "telemetry filters pull the latest ping from a type");
is(telemetry.testbar.payload.bar, 2, "telemetry filters pull from submitted telemetry pings");
});
add_task(async function testUserId() {
- let environment = ClientEnvironment.getEnvironment();
-
// Test that userId is available
- ok(UUID_REGEX.test(environment.userId), "userId available");
+ ok(UUID_REGEX.test(ClientEnvironment.userId), "userId available");
// test that it pulls from the right preference
await SpecialPowers.pushPrefEnv({set: [["app.normandy.user_id", "fake id"]]});
- environment = ClientEnvironment.getEnvironment();
- is(environment.userId, "fake id", "userId is pulled from preferences");
+ is(ClientEnvironment.userId, "fake id", "userId is pulled from preferences");
});
add_task(async function testDistribution() {
- let environment = ClientEnvironment.getEnvironment();
-
// distribution id defaults to "default"
- is(environment.distribution, "default", "distribution has a default value");
+ is(ClientEnvironment.distribution, "default", "distribution has a default value");
// distribution id is read from a preference
await SpecialPowers.pushPrefEnv({set: [["distribution.id", "funnelcake"]]});
- environment = ClientEnvironment.getEnvironment();
- is(environment.distribution, "funnelcake", "distribution is read from preferences");
+ is(ClientEnvironment.distribution, "funnelcake", "distribution is read from preferences");
});
const mockClassify = {country: "FR", request_time: new Date(2017, 1, 1)};
add_task(ClientEnvironment.withMockClassify(mockClassify, async function testCountryRequestTime() {
- const environment = ClientEnvironment.getEnvironment();
-
// Test that country and request_time pull their data from the server.
- is(await environment.country, mockClassify.country, "country is read from the server API");
+ is(await ClientEnvironment.country, mockClassify.country, "country is read from the server API");
is(
- await environment.request_time, mockClassify.request_time,
+ await ClientEnvironment.request_time, mockClassify.request_time,
"request_time is read from the server API"
);
}));
add_task(async function testSync() {
- let environment = ClientEnvironment.getEnvironment();
- is(environment.syncMobileDevices, 0, "syncMobileDevices defaults to zero");
- is(environment.syncDesktopDevices, 0, "syncDesktopDevices defaults to zero");
- is(environment.syncTotalDevices, 0, "syncTotalDevices defaults to zero");
+ is(ClientEnvironment.syncMobileDevices, 0, "syncMobileDevices defaults to zero");
+ is(ClientEnvironment.syncDesktopDevices, 0, "syncDesktopDevices defaults to zero");
+ is(ClientEnvironment.syncTotalDevices, 0, "syncTotalDevices defaults to zero");
await SpecialPowers.pushPrefEnv({
set: [
["services.sync.clients.devices.mobile", 5],
["services.sync.clients.devices.desktop", 4],
],
});
- environment = ClientEnvironment.getEnvironment();
- is(environment.syncMobileDevices, 5, "syncMobileDevices is read when set");
- is(environment.syncDesktopDevices, 4, "syncDesktopDevices is read when set");
- is(environment.syncTotalDevices, 9, "syncTotalDevices is read when set");
+ is(ClientEnvironment.syncMobileDevices, 5, "syncMobileDevices is read when set");
+ is(ClientEnvironment.syncDesktopDevices, 4, "syncDesktopDevices is read when set");
+ is(ClientEnvironment.syncTotalDevices, 9, "syncTotalDevices is read when set");
});
add_task(async function testDoNotTrack() {
- let environment = ClientEnvironment.getEnvironment();
-
// doNotTrack defaults to false
- ok(!environment.doNotTrack, "doNotTrack has a default value");
+ ok(!ClientEnvironment.doNotTrack, "doNotTrack has a default value");
// doNotTrack is read from a preference
await SpecialPowers.pushPrefEnv({set: [["privacy.donottrackheader.enabled", true]]});
- environment = ClientEnvironment.getEnvironment();
- ok(environment.doNotTrack, "doNotTrack is read from preferences");
+ ok(ClientEnvironment.doNotTrack, "doNotTrack is read from preferences");
});
add_task(async function testExperiments() {
const active = {name: "active", expired: false};
const expired = {name: "expired", expired: true};
const getAll = sinon.stub(PreferenceExperiments, "getAll", async () => [active, expired]);
- const environment = ClientEnvironment.getEnvironment();
- const experiments = await environment.experiments;
+ const experiments = await ClientEnvironment.experiments;
Assert.deepEqual(
experiments.all,
["active", "expired"],
"experiments.all returns all stored experiment names",
);
Assert.deepEqual(
experiments.active,
["active"],
@@ -118,27 +103,25 @@ add_task(async function testExperiments(
});
add_task(withDriver(Assert, async function testAddonsInContext(driver) {
// Create before install so that the listener is added before startup completes.
const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com");
const addonId = await driver.addons.install(TEST_XPI_URL);
await startupPromise;
- const environment = ClientEnvironment.getEnvironment();
- const addons = await environment.addons;
+ const addons = await ClientEnvironment.addons;
Assert.deepEqual(addons[addonId], {
id: [addonId],
name: "normandy_fixture",
version: "1.0",
installDate: addons[addonId].installDate,
isActive: true,
type: "extension",
}, "addons should be available in context");
await driver.addons.uninstall(addonId);
}));
add_task(async function isFirstRun() {
await SpecialPowers.pushPrefEnv({set: [["app.normandy.first_run", true]]});
- const environment = ClientEnvironment.getEnvironment();
- ok(environment.isFirstRun, "isFirstRun is read from preferences");
+ ok(ClientEnvironment.isFirstRun, "isFirstRun is read from preferences");
});
--- a/toolkit/components/normandy/test/browser/browser_FilterExpressions.js
+++ b/toolkit/components/normandy/test/browser/browser_FilterExpressions.js
@@ -136,16 +136,26 @@ add_task(async function testKeys() {
undefined,
"keys returns undefined for numbers",
);
is(
await FilterExpressions.eval("ctxObject|keys", {ctxObject: null}),
undefined,
"keys returns undefined for null",
);
+
+ // Object properties are not cached
+ let pong = 0;
+ context = {ctxObject: {
+ get ping() {
+ return ++pong;
+ }
+ }};
+ await FilterExpressions.eval("ctxObject.ping == 0 || ctxObject.ping == 1", context);
+ is(pong, 2, "Properties are not reifed");
});
// intersect tests
add_task(async function testIntersect() {
let val;
val = await FilterExpressions.eval("[1, 2, 3] intersect [4, 2, 6, 7, 3]");
Assert.deepEqual(
--- a/toolkit/components/normandy/test/browser/browser_RecipeRunner.js
+++ b/toolkit/components/normandy/test/browser/browser_RecipeRunner.js
@@ -44,16 +44,23 @@ add_task(async function getFilterContext
"normandy.recipe is the recipe passed to getFilterContext",
);
delete recipe.unrelated;
Assert.deepEqual(
context.normandy.recipe,
recipe,
"normandy.recipe drops unrecognized attributes from the recipe",
);
+
+ // Filter context attributes are cached.
+ await SpecialPowers.pushPrefEnv({set: [["app.normandy.user_id", "some id"]]});
+ is(context.normandy.userId, "some id", "User id is read from prefs when accessed");
+ await SpecialPowers.pushPrefEnv({set: [["app.normandy.user_id", "real id"]]});
+ is(context.normandy.userId, "some id", "userId was cached");
+
});
add_task(async function checkFilter() {
const check = filter => RecipeRunner.checkFilter({filter_expression: filter});
// Errors must result in a false return value.
ok(!(await check("invalid ( + 5yntax")), "Invalid filter expressions return false");
--- a/toolkit/components/normandy/test/browser/head.js
+++ b/toolkit/components/normandy/test/browser/head.js
@@ -2,17 +2,16 @@ ChromeUtils.import("resource://gre/modul
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
ChromeUtils.import("resource://testing-common/TestUtils.jsm", this);
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
ChromeUtils.import("resource://normandy/lib/Addons.jsm", this);
ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyDriver.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
-ChromeUtils.import("resource://normandy/lib/Utils.jsm", this);
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/docs/
/* global sinon */
Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
// Make sinon assertions fail in a way that mochitest understands
sinon.assert.fail = function(message) {
deleted file mode 100644
--- a/toolkit/components/normandy/test/unit/test_Utils.js
+++ /dev/null
@@ -1,20 +0,0 @@
-"use strict";
-
-ChromeUtils.import("resource://normandy/lib/Utils.jsm");
-
-add_task(async function testKeyBy() {
- const list = [];
- deepEqual(Utils.keyBy(list, "foo"), {});
-
- const foo = {name: "foo", value: 1};
- list.push(foo);
- deepEqual(Utils.keyBy(list, "name"), {foo});
-
- const bar = {name: "bar", value: 2};
- list.push(bar);
- deepEqual(Utils.keyBy(list, "name"), {foo, bar});
-
- const missingKey = {value: 7};
- list.push(missingKey);
- deepEqual(Utils.keyBy(list, "name"), {foo, bar}, "keyBy skips items that are missing the key");
-});
--- a/toolkit/components/normandy/test/unit/xpcshell.ini
+++ b/toolkit/components/normandy/test/unit/xpcshell.ini
@@ -1,13 +1,13 @@
[DEFAULT]
head = head_xpc.js
support-files =
mock_api/**
invalid_recipe_signature_api/**
query_server.sjs
echo_server.sjs
utils.js
+tags = normandy
[test_NormandyApi.js]
[test_Sampling.js]
[test_SandboxManager.js]
-[test_Utils.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/utils/ClientEnvironment.jsm
@@ -0,0 +1,126 @@
+/* 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";
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(this, "ShellService", "resource:///modules/ShellService.jsm");
+ChromeUtils.defineModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "TelemetryArchive", "resource://gre/modules/TelemetryArchive.jsm");
+ChromeUtils.defineModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+
+var EXPORTED_SYMBOLS = ["ClientEnvironmentBase"];
+
+
+/**
+ * Create an object that provides general information about the client application.
+ *
+ * Components like Normandy RecipeRunner use this as part of the context for filter expressions,
+ * so avoid adding non-getter functions as attributes, as filter expressions
+ * cannot execute functions.
+ *
+ * Also note that, because filter expressions implicitly resolve promises, you
+ * can add getter functions that return promises for async data.
+ */
+class ClientEnvironmentBase {
+ static get distribution() {
+ return Services.prefs.getCharPref("distribution.id", "default");
+ }
+
+ static get telemetry() {
+ return (async () => {
+ const pings = await TelemetryArchive.promiseArchivedPingList();
+
+ // get most recent ping per type
+ const mostRecentPings = {};
+ for (const ping of pings) {
+ if (ping.type in mostRecentPings) {
+ if (mostRecentPings[ping.type].timestampCreated < ping.timestampCreated) {
+ mostRecentPings[ping.type] = ping;
+ }
+ } else {
+ mostRecentPings[ping.type] = ping;
+ }
+ }
+
+ const telemetry = {};
+ for (const key in mostRecentPings) {
+ const ping = mostRecentPings[key];
+ telemetry[ping.type] = await TelemetryArchive.promiseArchivedPingById(ping.id);
+ }
+ return telemetry;
+ })();
+ }
+
+ static get version() {
+ return AppConstants.MOZ_APP_VERSION_DISPLAY;
+ }
+
+ static get channel() {
+ return UpdateUtils.getUpdateChannel(false);
+ }
+
+ static get isDefaultBrowser() {
+ return ShellService.isDefaultBrowser();
+ }
+
+ static get searchEngine() {
+ return (async () => {
+ const searchInitialized = await new Promise(resolve => Services.search.init(resolve));
+ if (Components.isSuccessCode(searchInitialized)) {
+ return Services.search.defaultEngine.identifier;
+ }
+ return null;
+ })();
+ }
+
+ static get syncSetup() {
+ return Services.prefs.prefHasUserValue("services.sync.username");
+ }
+
+ static get syncDesktopDevices() {
+ return Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0);
+ }
+
+ static get syncMobileDevices() {
+ return Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0);
+ }
+
+ static get syncTotalDevices() {
+ return this.syncDesktopDevices + this.syncMobileDevices;
+ }
+
+ static get addons() {
+ return (async () => {
+ const addons = await AddonManager.getAllAddons();
+ return addons.reduce((acc, addon) => {
+ const { id, isActive, name, type, version, installDate: installDateN } = addon;
+ const installDate = new Date(installDateN);
+ acc[id] = { id, isActive, name, type, version, installDate };
+ return acc;
+ }, {});
+ })();
+ }
+
+ static get plugins() {
+ return (async () => {
+ const plugins = await AddonManager.getAddonsByTypes(["plugin"]);
+ return plugins.reduce((acc, plugin) => {
+ const { name, description, version } = plugin;
+ acc[name] = { name, description, version };
+ return acc;
+ }, {});
+ })();
+ }
+
+ static get locale() {
+ return Services.locale.getAppLocaleAsLangTag();
+ }
+
+ static get doNotTrack() {
+ return Services.prefs.getBoolPref("privacy.donottrackheader.enabled", false);
+ }
+}
--- a/toolkit/components/utils/moz.build
+++ b/toolkit/components/utils/moz.build
@@ -6,8 +6,12 @@
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'General')
EXTRA_COMPONENTS += [
'simpleServices.js',
'utils.manifest',
]
+
+EXTRA_JS_MODULES['components-utils'] = [
+ 'ClientEnvironment.jsm'
+]