1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 var EXPORTED_SYMBOLS = [
12 "remoteSettingsBroadcastHandler",
15 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
16 const { XPCOMUtils } = ChromeUtils.import(
17 "resource://gre/modules/XPCOMUtils.jsm"
20 XPCOMUtils.defineLazyModuleGetters(this, {
21 UptakeTelemetry: "resource://services-common/uptake-telemetry.js",
22 pushBroadcastService: "resource://gre/modules/PushBroadcastService.jsm",
23 RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
24 Utils: "resource://services-settings/Utils.jsm",
26 "resource://gre/modules/components-utils/FilterExpressions.jsm",
27 RemoteSettingsWorker: "resource://services-settings/RemoteSettingsWorker.jsm",
30 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
32 const PREF_SETTINGS_DEFAULT_BUCKET = "services.settings.default_bucket";
33 const PREF_SETTINGS_BRANCH = "services.settings.";
34 const PREF_SETTINGS_DEFAULT_SIGNER = "default_signer";
35 const PREF_SETTINGS_SERVER_BACKOFF = "server.backoff";
36 const PREF_SETTINGS_LAST_UPDATE = "last_update_seconds";
37 const PREF_SETTINGS_LAST_ETAG = "last_etag";
38 const PREF_SETTINGS_CLOCK_SKEW_SECONDS = "clock_skew_seconds";
40 // Telemetry identifiers.
41 const TELEMETRY_COMPONENT = "remotesettings";
42 const TELEMETRY_SOURCE_POLL = "settings-changes-monitoring";
43 const TELEMETRY_SOURCE_SYNC = "settings-sync";
46 const BROADCAST_ID = "remote-settings/monitor_changes";
48 // Signer to be used when not specified (see Ci.nsIContentSignatureVerifier).
49 const DEFAULT_SIGNER = "remote-settings.content-signature.mozilla.org";
51 XPCOMUtils.defineLazyGetter(this, "gPrefs", () => {
52 return Services.prefs.getBranch(PREF_SETTINGS_BRANCH);
54 XPCOMUtils.defineLazyGetter(this, "console", () => Utils.log);
57 * Default entry filtering function, in charge of excluding remote settings entries
58 * where the JEXL expression evaluates into a falsy value.
59 * @param {Object} entry The Remote Settings entry to be excluded or kept.
60 * @param {ClientEnvironment} environment Information about version, language, platform etc.
61 * @returns {?Object} the entry or null if excluded.
63 async function jexlFilterFunc(entry, environment) {
64 const { filter_expression } = entry;
65 if (!filter_expression) {
73 result = await FilterExpressions.eval(filter_expression, context);
77 return result ? entry : null;
80 function remoteSettingsFunction() {
81 const _clients = new Map();
82 let _invalidatePolling = false;
84 // If not explicitly specified, use the default signer.
85 const defaultOptions = {
86 bucketNamePref: PREF_SETTINGS_DEFAULT_BUCKET,
87 signerName: DEFAULT_SIGNER,
88 filterFunc: jexlFilterFunc,
92 * RemoteSettings constructor.
94 * @param {String} collectionName The remote settings identifier
95 * @param {Object} options Advanced options
96 * @returns {RemoteSettingsClient} An instance of a Remote Settings client.
98 const remoteSettings = function(collectionName, options) {
99 // Get or instantiate a remote settings client.
100 if (!_clients.has(collectionName)) {
101 // Register a new client!
102 const c = new RemoteSettingsClient(collectionName, {
106 // Store instance for later call.
107 _clients.set(collectionName, c);
108 // Invalidate the polling status, since we want the new collection to
109 // be taken into account.
110 _invalidatePolling = true;
111 console.debug(`Instantiated new client ${c.identifier}`);
113 return _clients.get(collectionName);
117 * Internal helper to retrieve existing instances of clients or new instances
118 * with default options if possible, or `null` if bucket/collection are unknown.
120 async function _client(bucketName, collectionName) {
121 // Check if a client was registered for this bucket/collection. Potentially
122 // with some specific options like signer, filter function etc.
123 const client = _clients.get(collectionName);
124 if (client && client.bucketName == bucketName) {
127 // There was no client registered for this collection, but it's the main bucket,
128 // therefore we can instantiate a client with the default options.
129 // So if we have a local database or if we ship a JSON dump, then it means that
130 // this client is known but it was not registered yet (eg. calling module not "imported" yet).
132 bucketName == Services.prefs.getCharPref(PREF_SETTINGS_DEFAULT_BUCKET)
134 const c = new RemoteSettingsClient(collectionName, defaultOptions);
135 const [dbExists, localDump] = await Promise.all([
136 Utils.hasLocalData(c),
137 Utils.hasLocalDump(bucketName, collectionName),
139 if (dbExists || localDump) {
143 // Else, we cannot return a client insttance because we are not able to synchronize data in specific buckets.
144 // Mainly because we cannot guess which `signerName` has to be used for example.
145 // And we don't want to synchronize data for collections in the main bucket that are
146 // completely unknown (ie. no database and no JSON dump).
147 console.debug(`No known client for ${bucketName}/${collectionName}`);
152 * Main polling method, called by the ping mechanism.
154 * @param {Object} options
155 . * @param {Object} options.expectedTimestamp (optional) The expected timestamp to be received — used by servers for cache busting.
156 * @param {string} options.trigger (optional) label to identify what triggered this sync (eg. ``"timer"``, default: `"manual"`)
157 * @param {bool} options.full (optional) Ignore last polling status and fetch all changes (default: `false`)
158 * @returns {Promise} or throws error if something goes wrong.
160 remoteSettings.pollChanges = async ({
165 // When running in full mode, we ignore last polling status.
167 gPrefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
168 gPrefs.clearUserPref(PREF_SETTINGS_LAST_UPDATE);
169 gPrefs.clearUserPref(PREF_SETTINGS_LAST_ETAG);
172 let pollTelemetryArgs = {
173 source: TELEMETRY_SOURCE_POLL,
177 if (Utils.isOffline) {
178 console.info("Network is offline. Give up.");
179 await UptakeTelemetry.report(
181 UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR,
187 const startedAt = new Date();
189 // Check if the server backoff time is elapsed.
190 if (gPrefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF)) {
191 const backoffReleaseTime = gPrefs.getCharPref(
192 PREF_SETTINGS_SERVER_BACKOFF
194 const remainingMilliseconds =
195 parseInt(backoffReleaseTime, 10) - Date.now();
196 if (remainingMilliseconds > 0) {
197 // Backoff time has not elapsed yet.
198 await UptakeTelemetry.report(
200 UptakeTelemetry.STATUS.BACKOFF,
204 `Server is asking clients to back off; retry in ${Math.ceil(
205 remainingMilliseconds / 1000
209 gPrefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
213 console.info("Start polling for changes");
214 Services.obs.notifyObservers(
216 "remote-settings:changes-poll-start",
217 JSON.stringify({ expectedTimestamp })
220 // Do we have the latest version already?
221 // Every time we register a new client, we have to fetch the whole list again.
222 const lastEtag = _invalidatePolling
224 : gPrefs.getCharPref(PREF_SETTINGS_LAST_ETAG, "");
228 pollResult = await Utils.fetchLatestChanges(Utils.SERVER_URL, {
233 // Report polling error to Uptake Telemetry.
235 if (/JSON\.parse/.test(e.message)) {
236 reportStatus = UptakeTelemetry.STATUS.PARSE_ERROR;
237 } else if (/content-type/.test(e.message)) {
238 reportStatus = UptakeTelemetry.STATUS.CONTENT_ERROR;
239 } else if (/Server/.test(e.message)) {
240 reportStatus = UptakeTelemetry.STATUS.SERVER_ERROR;
241 } else if (/Timeout/.test(e.message)) {
242 reportStatus = UptakeTelemetry.STATUS.TIMEOUT_ERROR;
243 } else if (/NetworkError/.test(e.message)) {
244 reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
246 reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
248 await UptakeTelemetry.report(
253 // No need to go further.
254 throw new Error(`Polling for changes failed: ${e.message}.`);
265 // Report age of server data in Telemetry.
266 pollTelemetryArgs = { age: ageSeconds, ...pollTelemetryArgs };
268 // Report polling success to Uptake Telemetry.
271 ? UptakeTelemetry.STATUS.UP_TO_DATE
272 : UptakeTelemetry.STATUS.SUCCESS;
273 await UptakeTelemetry.report(
279 // Check if the server asked the clients to back off (for next poll).
280 if (backoffSeconds) {
282 "Server asks clients to backoff for ${backoffSeconds} seconds"
284 const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
285 gPrefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
288 // Record new update time and the difference between local and server time.
289 // Negative clockDifference means local time is behind server time
290 // by the absolute of that value in seconds (positive means it's ahead)
291 const clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
292 gPrefs.setIntPref(PREF_SETTINGS_CLOCK_SKEW_SECONDS, clockDifference);
293 const checkedServerTimeInSeconds = Math.round(serverTimeMillis / 1000);
294 gPrefs.setIntPref(PREF_SETTINGS_LAST_UPDATE, checkedServerTimeInSeconds);
296 // Iterate through the collections version info and initiate a synchronization
297 // on the related remote settings clients.
299 for (const change of changes) {
300 const { bucket, collection, last_modified } = change;
302 const client = await _client(bucket, collection);
304 // This collection has no associated client (eg. preview, other platform...)
307 // Start synchronization! It will be a no-op if the specified `lastModified` equals
308 // the one in the local database.
310 await client.maybeSync(last_modified, { trigger });
312 // Save last time this client was successfully synced.
313 Services.prefs.setIntPref(
314 client.lastCheckTimePref,
315 checkedServerTimeInSeconds
321 firstError.details = change;
327 _invalidatePolling = false;
329 // Report total synchronization duration to Telemetry.
330 const durationMilliseconds = new Date() - startedAt;
331 const syncTelemetryArgs = {
332 source: TELEMETRY_SOURCE_SYNC,
333 duration: durationMilliseconds,
334 timestamp: `${currentEtag}`,
339 // Report the global synchronization failure. Individual uptake reports will also have been sent for each collection.
340 await UptakeTelemetry.report(
342 UptakeTelemetry.STATUS.SYNC_ERROR,
345 // Notify potential observers of the error.
346 Services.obs.notifyObservers(
347 { wrappedJSObject: { error: firstError } },
348 "remote-settings:sync-error"
350 // Rethrow the first observed error
354 // Save current Etag for next poll.
356 gPrefs.setCharPref(PREF_SETTINGS_LAST_ETAG, currentEtag);
359 // Report the global synchronization success.
360 await UptakeTelemetry.report(
362 UptakeTelemetry.STATUS.SUCCESS,
366 console.info("Polling for changes done");
367 Services.obs.notifyObservers(null, "remote-settings:changes-poll-end");
371 * Returns an object with polling status information and the list of
372 * known remote settings collections.
374 remoteSettings.inspect = async () => {
377 currentEtag: serverTimestamp,
378 } = await Utils.fetchLatestChanges(Utils.SERVER_URL);
380 const collections = await Promise.all(
381 changes.map(async change => {
382 const { bucket, collection, last_modified: serverTimestamp } = change;
383 const client = await _client(bucket, collection);
387 const localTimestamp = await client.getLastModified();
388 const lastCheck = Services.prefs.getIntPref(
389 client.lastCheckTimePref,
398 signerName: client.signerName,
404 serverURL: Utils.SERVER_URL,
405 pollingEndpoint: Utils.SERVER_URL + Utils.CHANGES_PATH,
407 localTimestamp: gPrefs.getCharPref(PREF_SETTINGS_LAST_ETAG, null),
408 lastCheck: gPrefs.getIntPref(PREF_SETTINGS_LAST_UPDATE, 0),
409 mainBucket: Services.prefs.getCharPref(PREF_SETTINGS_DEFAULT_BUCKET),
410 defaultSigner: DEFAULT_SIGNER,
411 collections: collections.filter(c => !!c),
416 * Delete all local data, of every collection.
418 remoteSettings.clearAll = async () => {
419 const { collections } = await remoteSettings.inspect();
421 collections.map(async ({ collection }) => {
422 const client = RemoteSettings(collection);
423 // Delete all potential attachments.
424 await client.attachments.deleteAll();
425 // Delete local data.
426 await client.db.clear();
427 // Remove status pref.
428 Services.prefs.clearUserPref(client.lastCheckTimePref);
434 * Startup function called from nsBrowserGlue.
436 remoteSettings.init = () => {
437 console.info("Initialize Remote Settings");
438 // Hook the Push broadcast and RemoteSettings polling.
439 // When we start on a new profile there will be no ETag stored.
440 // Use an arbitrary ETag that is guaranteed not to occur.
441 // This will trigger a broadcast message but that's fine because we
442 // will check the changes on each collection and retrieve only the
443 // changes (e.g. nothing if we have a dump with the same data).
444 const currentVersion = gPrefs.getStringPref(PREF_SETTINGS_LAST_ETAG, '"0"');
447 symbolName: "remoteSettingsBroadcastHandler",
449 pushBroadcastService.addListener(BROADCAST_ID, currentVersion, moduleInfo);
452 return remoteSettings;
455 var RemoteSettings = remoteSettingsFunction();
457 var remoteSettingsBroadcastHandler = {
458 async receivedBroadcastMessage(version, broadcastID, context) {
459 const { phase } = context;
461 pushBroadcastService.PHASES.HELLO,
462 pushBroadcastService.PHASES.REGISTER,
466 `Push notification received (version=${version} phase=${phase})`
469 return RemoteSettings.pollChanges({
470 expectedTimestamp: version,
471 trigger: isStartup ? "startup" : "broadcast",