diff options
Diffstat (limited to '')
-rw-r--r-- | services/settings/Utils.jsm | 288 |
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; + }); + }, +}; |