summaryrefslogtreecommitdiffstats
path: root/services/settings/Utils.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/settings/Utils.jsm288
1 files changed, 288 insertions, 0 deletions
diff --git a/services/settings/Utils.jsm b/services/settings/Utils.jsm
new file mode 100644
index 0000000000..66df850904
--- /dev/null
+++ b/services/settings/Utils.jsm
@@ -0,0 +1,288 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["Utils"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "CaptivePortalService",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gNetworkLinkService",
+ "@mozilla.org/network/network-link-service;1",
+ "nsINetworkLinkService"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
+
+// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+// See LOG_LEVELS in Console.jsm. Common examples: "all", "debug", "info", "warn", "error".
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ const { ConsoleAPI } = ChromeUtils.import(
+ "resource://gre/modules/Console.jsm",
+ {}
+ );
+ return new ConsoleAPI({
+ maxLogLevel: "warn",
+ maxLogLevelPref: "services.settings.loglevel",
+ prefix: "services.settings",
+ });
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gServerURL",
+ "services.settings.server"
+);
+
+function _isUndefined(value) {
+ return typeof value === "undefined";
+}
+
+var Utils = {
+ get SERVER_URL() {
+ const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR");
+ return AppConstants.RELEASE_OR_BETA && !Cu.isInAutomation && !isXpcshell
+ ? "https://firefox.settings.services.mozilla.com/v1"
+ : gServerURL;
+ },
+
+ CHANGES_PATH: "/buckets/monitor/collections/changes/records",
+
+ /**
+ * Logger instance.
+ */
+ log,
+
+ /**
+ * Check if network is down.
+ *
+ * Note that if this returns false, it does not guarantee
+ * that network is up.
+ *
+ * @return {bool} Whether network is down or not.
+ */
+ get isOffline() {
+ try {
+ return (
+ Services.io.offline ||
+ CaptivePortalService.state == CaptivePortalService.LOCKED_PORTAL ||
+ !gNetworkLinkService.isLinkUp
+ );
+ } catch (ex) {
+ log.warn("Could not determine network status.", ex);
+ }
+ return false;
+ },
+
+ /**
+ * Check if local data exist for the specified client.
+ *
+ * @param {RemoteSettingsClient} client
+ * @return {bool} Whether it exists or not.
+ */
+ async hasLocalData(client) {
+ const timestamp = await client.db.getLastModified();
+ // Note: timestamp will be 0 if empty JSON dump is loaded.
+ return timestamp !== null;
+ },
+
+ /**
+ * Check if we ship a JSON dump for the specified bucket and collection.
+ *
+ * @param {String} bucket
+ * @param {String} collection
+ * @return {bool} Whether it is present or not.
+ */
+ async hasLocalDump(bucket, collection) {
+ try {
+ await fetch(
+ `resource://app/defaults/settings/${bucket}/${collection}.json`
+ );
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Fetch the list of remote collections and their timestamp.
+ * @param {String} serverUrl The server URL (eg. `https://server.org/v1`)
+ * @param {int} expectedTimestamp The timestamp that the server is supposed to return.
+ * We obtained it from the Megaphone notification payload,
+ * and we use it only for cache busting (Bug 1497159).
+ * @param {String} lastEtag (optional) The Etag of the latest poll to be matched
+ * by the server (eg. `"123456789"`).
+ * @param {Object} filters
+ */
+ async fetchLatestChanges(serverUrl, options = {}) {
+ const { expectedTimestamp, lastEtag = "", filters = {} } = options;
+
+ //
+ // Fetch the list of changes objects from the server that looks like:
+ // {"data":[
+ // {
+ // "host":"kinto-ota.dev.mozaws.net",
+ // "last_modified":1450717104423,
+ // "bucket":"blocklists",
+ // "collection":"certificates"
+ // }]}
+
+ let url = serverUrl + Utils.CHANGES_PATH;
+
+ // Use ETag to obtain a `304 Not modified` when no change occurred,
+ // and `?_since` parameter to only keep entries that weren't processed yet.
+ const headers = {};
+ const params = { ...filters };
+ if (lastEtag != "") {
+ headers["If-None-Match"] = lastEtag;
+ params._since = lastEtag;
+ }
+ if (expectedTimestamp) {
+ params._expected = expectedTimestamp;
+ }
+ if (params) {
+ url +=
+ "?" +
+ Object.entries(params)
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
+ .join("&");
+ }
+ const response = await fetch(url, { headers });
+
+ let changes = [];
+ // If no changes since last time, go on with empty list of changes.
+ if (response.status != 304) {
+ if (response.status >= 500) {
+ throw new Error(
+ `Server error ${response.status} ${response.statusText}`
+ );
+ }
+
+ const is404FromCustomServer =
+ response.status == 404 &&
+ Services.prefs.prefHasUserValue("services.settings.server");
+
+ const ct = response.headers.get("Content-Type");
+ if (!is404FromCustomServer && (!ct || !ct.includes("application/json"))) {
+ throw new Error(`Unexpected content-type "${ct}"`);
+ }
+ let payload;
+ try {
+ payload = await response.json();
+ } catch (e) {
+ payload = e.message;
+ }
+
+ if (!payload.hasOwnProperty("data")) {
+ // If the server is failing, the JSON response might not contain the
+ // expected data. For example, real server errors (Bug 1259145)
+ // or dummy local server for tests (Bug 1481348)
+ if (!is404FromCustomServer) {
+ throw new Error(
+ `Server error ${response.status} ${
+ response.statusText
+ }: ${JSON.stringify(payload)}`
+ );
+ }
+ } else {
+ changes = payload.data;
+ }
+ }
+ // The server should always return ETag. But we've had situations where the CDN
+ // was interfering.
+ const currentEtag = response.headers.has("ETag")
+ ? response.headers.get("ETag")
+ : undefined;
+ let serverTimeMillis = Date.parse(response.headers.get("Date"));
+ // Since the response is served via a CDN, the Date header value could have been cached.
+ const cacheAgeSeconds = response.headers.has("Age")
+ ? parseInt(response.headers.get("Age"), 10)
+ : 0;
+ serverTimeMillis += cacheAgeSeconds * 1000;
+
+ // Age of data (time between publication and now).
+ let lastModifiedMillis = Date.parse(response.headers.get("Last-Modified"));
+ const ageSeconds = (serverTimeMillis - lastModifiedMillis) / 1000;
+
+ // Check if the server asked the clients to back off.
+ let backoffSeconds;
+ if (response.headers.has("Backoff")) {
+ const value = parseInt(response.headers.get("Backoff"), 10);
+ if (!isNaN(value)) {
+ backoffSeconds = value;
+ }
+ }
+
+ return {
+ changes,
+ currentEtag,
+ serverTimeMillis,
+ backoffSeconds,
+ ageSeconds,
+ };
+ },
+
+ /**
+ * Test if a single object matches all given filters.
+ *
+ * @param {Object} filters The filters object.
+ * @param {Object} entry The object to filter.
+ * @return {Boolean}
+ */
+ filterObject(filters, entry) {
+ return Object.entries(filters).every(([filter, value]) => {
+ if (Array.isArray(value)) {
+ return value.some(candidate => candidate === entry[filter]);
+ } else if (typeof value === "object") {
+ return Utils.filterObject(value, entry[filter]);
+ } else if (!Object.prototype.hasOwnProperty.call(entry, filter)) {
+ console.error(`The property ${filter} does not exist`);
+ return false;
+ }
+ return entry[filter] === value;
+ });
+ },
+
+ /**
+ * Sorts records in a list according to a given ordering.
+ *
+ * @param {String} order The ordering, eg. `-last_modified`.
+ * @param {Array} list The collection to order.
+ * @return {Array}
+ */
+ sortObjects(order, list) {
+ const hasDash = order[0] === "-";
+ const field = hasDash ? order.slice(1) : order;
+ const direction = hasDash ? -1 : 1;
+ return list.slice().sort((a, b) => {
+ if (a[field] && _isUndefined(b[field])) {
+ return direction;
+ }
+ if (b[field] && _isUndefined(a[field])) {
+ return -direction;
+ }
+ if (_isUndefined(a[field]) && _isUndefined(b[field])) {
+ return 0;
+ }
+ return a[field] > b[field] ? direction : -direction;
+ });
+ },
+};