summaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/automation/ServicesAutomation.sys.mjs6
-rw-r--r--services/common/async.sys.mjs2
-rw-r--r--services/common/hawkrequest.sys.mjs2
-rw-r--r--services/common/kinto-offline-client.js2643
-rw-r--r--services/common/kinto-offline-client.sys.mjs2613
-rw-r--r--services/common/kinto-storage-adapter.sys.mjs4
-rw-r--r--services/common/modules-testing/logging.sys.mjs2
-rw-r--r--services/common/moz.build2
-rw-r--r--services/common/tests/unit/head_helpers.js6
-rw-r--r--services/common/tests/unit/test_kinto.js4
-rw-r--r--services/common/tests/unit/test_utils_namedTimer.js4
-rw-r--r--services/crypto/modules/WeaveCrypto.sys.mjs2
-rw-r--r--services/crypto/tests/unit/head_helpers.js2
-rw-r--r--services/fxaccounts/FxAccounts.sys.mjs52
-rw-r--r--services/fxaccounts/FxAccountsClient.sys.mjs4
-rw-r--r--services/fxaccounts/FxAccountsProfile.sys.mjs2
-rw-r--r--services/fxaccounts/FxAccountsWebChannel.sys.mjs2
-rw-r--r--services/fxaccounts/tests/mochitest/test_invalidEmailCase.html2
-rw-r--r--services/fxaccounts/tests/xpcshell/test_accounts.js60
-rw-r--r--services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js9
-rw-r--r--services/fxaccounts/tests/xpcshell/test_commands.js6
-rw-r--r--services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js2
-rw-r--r--services/fxaccounts/tests/xpcshell/test_pairing.js2
-rw-r--r--services/fxaccounts/tests/xpcshell/test_profile.js10
-rw-r--r--services/fxaccounts/tests/xpcshell/test_profile_client.js12
-rw-r--r--services/fxaccounts/tests/xpcshell/test_push_service.js4
-rw-r--r--services/fxaccounts/tests/xpcshell/test_storage_manager.js2
-rw-r--r--services/fxaccounts/tests/xpcshell/test_web_channel.js8
-rw-r--r--services/settings/Attachments.sys.mjs69
-rw-r--r--services/settings/IDBHelpers.sys.mjs2
-rw-r--r--services/settings/RemoteSettings.worker.mjs2
-rw-r--r--services/settings/RemoteSettingsClient.sys.mjs2
-rw-r--r--services/settings/RemoteSettingsComponents.sys.mjs2
-rw-r--r--services/settings/docs/index.rst6
-rw-r--r--services/settings/dumps/blocklists/addons-bloomfilters.json287
-rw-r--r--services/settings/dumps/gen_last_modified.py3
-rw-r--r--services/settings/dumps/main/cookie-banner-rules-list.json868
-rw-r--r--services/settings/dumps/main/devtools-compatibility-browsers.json254
-rw-r--r--services/settings/dumps/main/search-telemetry-v2.json444
-rw-r--r--services/settings/dumps/main/translations-models.json110
-rw-r--r--services/settings/dumps/security-state/intermediates.json398
-rw-r--r--services/settings/test/unit/test_attachments_downloader.js82
-rw-r--r--services/settings/test/unit/test_remote_settings.js20
-rw-r--r--services/settings/test/unit/test_remote_settings_dump_lastmodified.js8
-rw-r--r--services/settings/test/unit/test_remote_settings_offline.js12
-rw-r--r--services/settings/test/unit/test_remote_settings_poll.js12
-rw-r--r--services/settings/test/unit/test_remote_settings_signatures.js6
-rw-r--r--services/settings/test/unit/test_remote_settings_worker.js4
-rw-r--r--services/settings/test/unit/test_shutdown_handling.js4
-rw-r--r--services/sync/Weave.sys.mjs2
-rw-r--r--services/sync/docs/engines.rst4
-rw-r--r--services/sync/modules-testing/fakeservices.sys.mjs16
-rw-r--r--services/sync/modules-testing/fxa_utils.sys.mjs2
-rw-r--r--services/sync/modules/UIState.sys.mjs2
-rw-r--r--services/sync/modules/bridged_engine.sys.mjs4
-rw-r--r--services/sync/modules/collection_validator.sys.mjs4
-rw-r--r--services/sync/modules/constants.sys.mjs2
-rw-r--r--services/sync/modules/engines.sys.mjs30
-rw-r--r--services/sync/modules/engines/bookmarks.sys.mjs8
-rw-r--r--services/sync/modules/engines/clients.sys.mjs2
-rw-r--r--services/sync/modules/engines/extension-storage.sys.mjs4
-rw-r--r--services/sync/modules/engines/forms.sys.mjs2
-rw-r--r--services/sync/modules/engines/prefs.sys.mjs6
-rw-r--r--services/sync/modules/engines/tabs.sys.mjs4
-rw-r--r--services/sync/modules/record.sys.mjs6
-rw-r--r--services/sync/modules/sync_auth.sys.mjs35
-rw-r--r--services/sync/modules/telemetry.sys.mjs4
-rw-r--r--services/sync/tests/tps/.eslintrc.js2
-rw-r--r--services/sync/tests/unit/head_helpers.js4
-rw-r--r--services/sync/tests/unit/head_http_server.js6
-rw-r--r--services/sync/tests/unit/test_addon_utils.js2
-rw-r--r--services/sync/tests/unit/test_addons_validator.js2
-rw-r--r--services/sync/tests/unit/test_bookmark_engine.js2
-rw-r--r--services/sync/tests/unit/test_bookmark_tracker.js14
-rw-r--r--services/sync/tests/unit/test_clients_engine.js27
-rw-r--r--services/sync/tests/unit/test_declined.js4
-rw-r--r--services/sync/tests/unit/test_engine_abort.js2
-rw-r--r--services/sync/tests/unit/test_errorhandler_1.js22
-rw-r--r--services/sync/tests/unit/test_errorhandler_2.js77
-rw-r--r--services/sync/tests/unit/test_errorhandler_filelog.js2
-rw-r--r--services/sync/tests/unit/test_fxa_node_reassignment.js2
-rw-r--r--services/sync/tests/unit/test_history_engine.js4
-rw-r--r--services/sync/tests/unit/test_history_store.js2
-rw-r--r--services/sync/tests/unit/test_history_tracker.js4
-rw-r--r--services/sync/tests/unit/test_hmac_error.js2
-rw-r--r--services/sync/tests/unit/test_httpd_sync_server.js2
-rw-r--r--services/sync/tests/unit/test_interval_triggers.js2
-rw-r--r--services/sync/tests/unit/test_password_engine.js9
-rw-r--r--services/sync/tests/unit/test_resource.js2
-rw-r--r--services/sync/tests/unit/test_service_sync_401.js2
-rw-r--r--services/sync/tests/unit/test_service_verifyLogin.js11
-rw-r--r--services/sync/tests/unit/test_sync_auth_manager.js35
-rw-r--r--services/sync/tests/unit/test_syncedtabs.js2
-rw-r--r--services/sync/tests/unit/test_syncscheduler.js2
-rw-r--r--services/sync/tests/unit/test_tab_quickwrite.js2
-rw-r--r--services/sync/tests/unit/test_telemetry.js4
-rw-r--r--services/sync/tests/unit/test_uistate.js4
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs2
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs2
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs2
-rw-r--r--services/sync/tps/extensions/tps/resource/tps.sys.mjs2
101 files changed, 4667 insertions, 3799 deletions
diff --git a/services/automation/ServicesAutomation.sys.mjs b/services/automation/ServicesAutomation.sys.mjs
index 24316d8827..fd5a58cba9 100644
--- a/services/automation/ServicesAutomation.sys.mjs
+++ b/services/automation/ServicesAutomation.sys.mjs
@@ -139,11 +139,11 @@ export var Authentication = {
let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
let newtab = mainWindow.gBrowser.addWebTab(uri);
let win = mainWindow.gBrowser.getBrowserForTab(newtab);
- win.addEventListener("load", function (e) {
+ win.addEventListener("load", function () {
LOG("load");
});
- win.addEventListener("loadstart", function (e) {
+ win.addEventListener("loadstart", function () {
LOG("loadstart");
});
@@ -299,7 +299,7 @@ export var Sync = {
await this.wipeLogs();
},
- observe(subject, topic, data) {
+ observe(subject, topic) {
LOG("Event received " + topic);
},
diff --git a/services/common/async.sys.mjs b/services/common/async.sys.mjs
index 564b46a071..3b9ec29833 100644
--- a/services/common/async.sys.mjs
+++ b/services/common/async.sys.mjs
@@ -289,7 +289,7 @@ class Watchdog {
}
}
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (topic == "timer-callback") {
this.abortReason = "timeout";
} else if (topic == "quit-application") {
diff --git a/services/common/hawkrequest.sys.mjs b/services/common/hawkrequest.sys.mjs
index a856ef032d..bd8d51bd73 100644
--- a/services/common/hawkrequest.sys.mjs
+++ b/services/common/hawkrequest.sys.mjs
@@ -162,7 +162,7 @@ Intl.prototype = {
Services.prefs.removeObserver("intl.accept_languages", this);
},
- observe(subject, topic, data) {
+ observe() {
this.readPref();
},
diff --git a/services/common/kinto-offline-client.js b/services/common/kinto-offline-client.js
deleted file mode 100644
index 7b11347555..0000000000
--- a/services/common/kinto-offline-client.js
+++ /dev/null
@@ -1,2643 +0,0 @@
-/*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-"use strict";
-
-/*
- * This file is generated from kinto.js - do not modify directly.
- */
-
-// This is required because with Babel compiles ES2015 modules into a
-// require() form that tries to keep its modules on "this", but
-// doesn't specify "this", leaving it to default to the global
-// object. However, in strict mode, "this" no longer defaults to the
-// global object, so expose the global object explicitly. Babel's
-// compiled output will use a variable called "global" if one is
-// present.
-//
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
-// more details.
-const global = this;
-
-var EXPORTED_SYMBOLS = ["Kinto"];
-
-/*
- * Version 13.0.0 - 7fbf95d
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global = global || self, global.Kinto = factory());
-}(this, (function () { 'use strict';
-
- /**
- * Base db adapter.
- *
- * @abstract
- */
- class BaseAdapter {
- /**
- * Deletes every records present in the database.
- *
- * @abstract
- * @return {Promise}
- */
- clear() {
- throw new Error("Not Implemented.");
- }
- /**
- * Executes a batch of operations within a single transaction.
- *
- * @abstract
- * @param {Function} callback The operation callback.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- execute(callback, options = { preload: [] }) {
- throw new Error("Not Implemented.");
- }
- /**
- * Retrieve a record by its primary key from the database.
- *
- * @abstract
- * @param {String} id The record id.
- * @return {Promise}
- */
- get(id) {
- throw new Error("Not Implemented.");
- }
- /**
- * Lists all records from the database.
- *
- * @abstract
- * @param {Object} params The filters and order to apply to the results.
- * @return {Promise}
- */
- list(params = { filters: {}, order: "" }) {
- throw new Error("Not Implemented.");
- }
- /**
- * Store the lastModified value.
- *
- * @abstract
- * @param {Number} lastModified
- * @return {Promise}
- */
- saveLastModified(lastModified) {
- throw new Error("Not Implemented.");
- }
- /**
- * Retrieve saved lastModified value.
- *
- * @abstract
- * @return {Promise}
- */
- getLastModified() {
- throw new Error("Not Implemented.");
- }
- /**
- * Load records in bulk that were exported from a server.
- *
- * @abstract
- * @param {Array} records The records to load.
- * @return {Promise}
- */
- importBulk(records) {
- throw new Error("Not Implemented.");
- }
- /**
- * Load a dump of records exported from a server.
- *
- * @deprecated Use {@link importBulk} instead.
- * @abstract
- * @param {Array} records The records to load.
- * @return {Promise}
- */
- loadDump(records) {
- throw new Error("Not Implemented.");
- }
- saveMetadata(metadata) {
- throw new Error("Not Implemented.");
- }
- getMetadata() {
- throw new Error("Not Implemented.");
- }
- }
-
- const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
- /**
- * Checks if a value is undefined.
- * @param {Any} value
- * @return {Boolean}
- */
- function _isUndefined(value) {
- return typeof value === "undefined";
- }
- /**
- * 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}
- */
- function 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;
- });
- }
- /**
- * Test if a single object matches all given filters.
- *
- * @param {Object} filters The filters object.
- * @param {Object} entry The object to filter.
- * @return {Boolean}
- */
- function filterObject(filters, entry) {
- return Object.keys(filters).every(filter => {
- const value = filters[filter];
- if (Array.isArray(value)) {
- return value.some(candidate => candidate === entry[filter]);
- }
- else if (typeof value === "object") {
- return 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;
- });
- }
- /**
- * Resolves a list of functions sequentially, which can be sync or async; in
- * case of async, functions must return a promise.
- *
- * @param {Array} fns The list of functions.
- * @param {Any} init The initial value.
- * @return {Promise}
- */
- function waterfall(fns, init) {
- if (!fns.length) {
- return Promise.resolve(init);
- }
- return fns.reduce((promise, nextFn) => {
- return promise.then(nextFn);
- }, Promise.resolve(init));
- }
- /**
- * Simple deep object comparison function. This only supports comparison of
- * serializable JavaScript objects.
- *
- * @param {Object} a The source object.
- * @param {Object} b The compared object.
- * @return {Boolean}
- */
- function deepEqual(a, b) {
- if (a === b) {
- return true;
- }
- if (typeof a !== typeof b) {
- return false;
- }
- if (!(a && typeof a == "object") || !(b && typeof b == "object")) {
- return false;
- }
- if (Object.keys(a).length !== Object.keys(b).length) {
- return false;
- }
- for (const k in a) {
- if (!deepEqual(a[k], b[k])) {
- return false;
- }
- }
- return true;
- }
- /**
- * Return an object without the specified keys.
- *
- * @param {Object} obj The original object.
- * @param {Array} keys The list of keys to exclude.
- * @return {Object} A copy without the specified keys.
- */
- function omitKeys(obj, keys = []) {
- const result = Object.assign({}, obj);
- for (const key of keys) {
- delete result[key];
- }
- return result;
- }
- function arrayEqual(a, b) {
- if (a.length !== b.length) {
- return false;
- }
- for (let i = a.length; i--;) {
- if (a[i] !== b[i]) {
- return false;
- }
- }
- return true;
- }
- function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
- const last = arr.length - 1;
- return arr.reduce((acc, cv, i) => {
- if (i === last) {
- return (acc[cv] = val);
- }
- else if (Object.prototype.hasOwnProperty.call(acc, cv)) {
- return acc[cv];
- }
- else {
- return (acc[cv] = {});
- }
- }, nestedFiltersObj);
- }
- function transformSubObjectFilters(filtersObj) {
- const transformedFilters = {};
- for (const key in filtersObj) {
- const keysArr = key.split(".");
- const val = filtersObj[key];
- makeNestedObjectFromArr(keysArr, val, transformedFilters);
- }
- return transformedFilters;
- }
-
- const INDEXED_FIELDS = ["id", "_status", "last_modified"];
- /**
- * Small helper that wraps the opening of an IndexedDB into a Promise.
- *
- * @param dbname {String} The database name.
- * @param version {Integer} Schema version
- * @param onupgradeneeded {Function} The callback to execute if schema is
- * missing or different.
- * @return {Promise<IDBDatabase>}
- */
- async function open(dbname, { version, onupgradeneeded }) {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(dbname, version);
- request.onupgradeneeded = event => {
- const db = event.target.result;
- db.onerror = event => reject(event.target.error);
- // When an upgrade is needed, a transaction is started.
- const transaction = event.target.transaction;
- transaction.onabort = event => {
- const error = event.target.error ||
- transaction.error ||
- new DOMException("The operation has been aborted", "AbortError");
- reject(error);
- };
- // Callback for store creation etc.
- return onupgradeneeded(event);
- };
- request.onerror = event => {
- reject(event.target.error);
- };
- request.onsuccess = event => {
- const db = event.target.result;
- resolve(db);
- };
- });
- }
- /**
- * Helper to run the specified callback in a single transaction on the
- * specified store.
- * The helper focuses on transaction wrapping into a promise.
- *
- * @param db {IDBDatabase} The database instance.
- * @param name {String} The store name.
- * @param callback {Function} The piece of code to execute in the transaction.
- * @param options {Object} Options.
- * @param options.mode {String} Transaction mode (default: read).
- * @return {Promise} any value returned by the callback.
- */
- async function execute(db, name, callback, options = {}) {
- const { mode } = options;
- return new Promise((resolve, reject) => {
- // On Safari, calling IDBDatabase.transaction with mode == undefined raises
- // a TypeError.
- const transaction = mode
- ? db.transaction([name], mode)
- : db.transaction([name]);
- const store = transaction.objectStore(name);
- // Let the callback abort this transaction.
- const abort = e => {
- transaction.abort();
- reject(e);
- };
- // Execute the specified callback **synchronously**.
- let result;
- try {
- result = callback(store, abort);
- }
- catch (e) {
- abort(e);
- }
- transaction.onerror = event => reject(event.target.error);
- transaction.oncomplete = event => resolve(result);
- transaction.onabort = event => {
- const error = event.target.error ||
- transaction.error ||
- new DOMException("The operation has been aborted", "AbortError");
- reject(error);
- };
- });
- }
- /**
- * Helper to wrap the deletion of an IndexedDB database into a promise.
- *
- * @param dbName {String} the database to delete
- * @return {Promise}
- */
- async function deleteDatabase(dbName) {
- return new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase(dbName);
- request.onsuccess = event => resolve(event.target);
- request.onerror = event => reject(event.target.error);
- });
- }
- /**
- * IDB cursor handlers.
- * @type {Object}
- */
- const cursorHandlers = {
- all(filters, done) {
- const results = [];
- return event => {
- const cursor = event.target.result;
- if (cursor) {
- const { value } = cursor;
- if (filterObject(filters, value)) {
- results.push(value);
- }
- cursor.continue();
- }
- else {
- done(results);
- }
- };
- },
- in(values, filters, done) {
- const results = [];
- let i = 0;
- return function (event) {
- const cursor = event.target.result;
- if (!cursor) {
- done(results);
- return;
- }
- const { key, value } = cursor;
- // `key` can be an array of two values (see `keyPath` in indices definitions).
- // `values` can be an array of arrays if we filter using an index whose key path
- // is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
- while (key > values[i]) {
- // The cursor has passed beyond this key. Check next.
- ++i;
- if (i === values.length) {
- done(results); // There is no next. Stop searching.
- return;
- }
- }
- const isEqual = Array.isArray(key)
- ? arrayEqual(key, values[i])
- : key === values[i];
- if (isEqual) {
- if (filterObject(filters, value)) {
- results.push(value);
- }
- cursor.continue();
- }
- else {
- cursor.continue(values[i]);
- }
- };
- },
- };
- /**
- * Creates an IDB request and attach it the appropriate cursor event handler to
- * perform a list query.
- *
- * Multiple matching values are handled by passing an array.
- *
- * @param {String} cid The collection id (ie. `{bid}/{cid}`)
- * @param {IDBStore} store The IDB store.
- * @param {Object} filters Filter the records by field.
- * @param {Function} done The operation completion handler.
- * @return {IDBRequest}
- */
- function createListRequest(cid, store, filters, done) {
- const filterFields = Object.keys(filters);
- // If no filters, get all results in one bulk.
- if (filterFields.length == 0) {
- const request = store.index("cid").getAll(IDBKeyRange.only(cid));
- request.onsuccess = event => done(event.target.result);
- return request;
- }
- // Introspect filters and check if they leverage an indexed field.
- const indexField = filterFields.find(field => {
- return INDEXED_FIELDS.includes(field);
- });
- if (!indexField) {
- // Iterate on all records for this collection (ie. cid)
- const isSubQuery = Object.keys(filters).some(key => key.includes(".")); // (ie. filters: {"article.title": "hello"})
- if (isSubQuery) {
- const newFilter = transformSubObjectFilters(filters);
- const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
- request.onsuccess = cursorHandlers.all(newFilter, done);
- return request;
- }
- const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
- request.onsuccess = cursorHandlers.all(filters, done);
- return request;
- }
- // If `indexField` was used already, don't filter again.
- const remainingFilters = omitKeys(filters, [indexField]);
- // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
- const value = filters[indexField];
- // For the "id" field, use the primary key.
- const indexStore = indexField == "id" ? store : store.index(indexField);
- // WHERE IN equivalent clause
- if (Array.isArray(value)) {
- if (value.length === 0) {
- return done([]);
- }
- const values = value.map(i => [cid, i]).sort();
- const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
- const request = indexStore.openCursor(range);
- request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
- return request;
- }
- // If no filters on custom attribute, get all results in one bulk.
- if (remainingFilters.length == 0) {
- const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
- request.onsuccess = event => done(event.target.result);
- return request;
- }
- // WHERE field = value clause
- const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
- request.onsuccess = cursorHandlers.all(remainingFilters, done);
- return request;
- }
- class IDBError extends Error {
- constructor(method, err) {
- super(`IndexedDB ${method}() ${err.message}`);
- this.name = err.name;
- this.stack = err.stack;
- }
- }
- /**
- * IndexedDB adapter.
- *
- * This adapter doesn't support any options.
- */
- class IDB extends BaseAdapter {
- /* Expose the IDBError class publicly */
- static get IDBError() {
- return IDBError;
- }
- /**
- * Constructor.
- *
- * @param {String} cid The key base for this collection (eg. `bid/cid`)
- * @param {Object} options
- * @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`)
- * @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
- */
- constructor(cid, options = {}) {
- super();
- this.cid = cid;
- this.dbName = options.dbName || "KintoDB";
- this._options = options;
- this._db = null;
- }
- _handleError(method, err) {
- throw new IDBError(method, err);
- }
- /**
- * Ensures a connection to the IndexedDB database has been opened.
- *
- * @override
- * @return {Promise}
- */
- async open() {
- if (this._db) {
- return this;
- }
- // In previous versions, we used to have a database with name `${bid}/${cid}`.
- // Check if it exists, and migrate data once new schema is in place.
- // Note: the built-in migrations from IndexedDB can only be used if the
- // database name does not change.
- const dataToMigrate = this._options.migrateOldData
- ? await migrationRequired(this.cid)
- : null;
- this._db = await open(this.dbName, {
- version: 2,
- onupgradeneeded: event => {
- const db = event.target.result;
- if (event.oldVersion < 1) {
- // Records store
- const recordsStore = db.createObjectStore("records", {
- keyPath: ["_cid", "id"],
- });
- // An index to obtain all the records in a collection.
- recordsStore.createIndex("cid", "_cid");
- // Here we create indices for every known field in records by collection.
- // Local record status ("synced", "created", "updated", "deleted")
- recordsStore.createIndex("_status", ["_cid", "_status"]);
- // Last modified field
- recordsStore.createIndex("last_modified", ["_cid", "last_modified"]);
- // Timestamps store
- db.createObjectStore("timestamps", {
- keyPath: "cid",
- });
- }
- if (event.oldVersion < 2) {
- // Collections store
- db.createObjectStore("collections", {
- keyPath: "cid",
- });
- }
- },
- });
- if (dataToMigrate) {
- const { records, timestamp } = dataToMigrate;
- await this.importBulk(records);
- await this.saveLastModified(timestamp);
- console.log(`${this.cid}: data was migrated successfully.`);
- // Delete the old database.
- await deleteDatabase(this.cid);
- console.warn(`${this.cid}: old database was deleted.`);
- }
- return this;
- }
- /**
- * Closes current connection to the database.
- *
- * @override
- * @return {Promise}
- */
- close() {
- if (this._db) {
- this._db.close(); // indexedDB.close is synchronous
- this._db = null;
- }
- return Promise.resolve();
- }
- /**
- * Returns a transaction and an object store for a store name.
- *
- * To determine if a transaction has completed successfully, we should rather
- * listen to the transaction’s complete event rather than the IDBObjectStore
- * request’s success event, because the transaction may still fail after the
- * success event fires.
- *
- * @param {String} name Store name
- * @param {Function} callback to execute
- * @param {Object} options Options
- * @param {String} options.mode Transaction mode ("readwrite" or undefined)
- * @return {Object}
- */
- async prepare(name, callback, options) {
- await this.open();
- await execute(this._db, name, callback, options);
- }
- /**
- * Deletes every records in the current collection.
- *
- * @override
- * @return {Promise}
- */
- async clear() {
- try {
- await this.prepare("records", store => {
- const range = IDBKeyRange.only(this.cid);
- const request = store.index("cid").openKeyCursor(range);
- request.onsuccess = event => {
- const cursor = event.target.result;
- if (cursor) {
- store.delete(cursor.primaryKey);
- cursor.continue();
- }
- };
- return request;
- }, { mode: "readwrite" });
- }
- catch (e) {
- this._handleError("clear", e);
- }
- }
- /**
- * Executes the set of synchronous CRUD operations described in the provided
- * callback within an IndexedDB transaction, for current db store.
- *
- * The callback will be provided an object exposing the following synchronous
- * CRUD operation methods: get, create, update, delete.
- *
- * Important note: because limitations in IndexedDB implementations, no
- * asynchronous code should be performed within the provided callback; the
- * promise will therefore be rejected if the callback returns a Promise.
- *
- * Options:
- * - {Array} preload: The list of record IDs to fetch and make available to
- * the transaction object get() method (default: [])
- *
- * @example
- * const db = new IDB("example");
- * const result = await db.execute(transaction => {
- * transaction.create({id: 1, title: "foo"});
- * transaction.update({id: 2, title: "bar"});
- * transaction.delete(3);
- * return "foo";
- * });
- *
- * @override
- * @param {Function} callback The operation description callback.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- async execute(callback, options = { preload: [] }) {
- // Transactions in IndexedDB are autocommited when a callback does not
- // perform any additional operation.
- // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
- // prevents using within an opened transaction.
- // To avoid managing asynchronocity in the specified `callback`, we preload
- // a list of record in order to execute the `callback` synchronously.
- // See also:
- // - http://stackoverflow.com/a/28388805/330911
- // - http://stackoverflow.com/a/10405196
- // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- let result;
- await this.prepare("records", (store, abort) => {
- const runCallback = (preloaded = []) => {
- // Expose a consistent API for every adapter instead of raw store methods.
- const proxy = transactionProxy(this, store, preloaded);
- // The callback is executed synchronously within the same transaction.
- try {
- const returned = callback(proxy);
- if (returned instanceof Promise) {
- // XXX: investigate how to provide documentation details in error.
- throw new Error("execute() callback should not return a Promise.");
- }
- // Bring to scope that will be returned (once promise awaited).
- result = returned;
- }
- catch (e) {
- // The callback has thrown an error explicitly. Abort transaction cleanly.
- abort(e);
- }
- };
- // No option to preload records, go straight to `callback`.
- if (!options.preload.length) {
- return runCallback();
- }
- // Preload specified records using a list request.
- const filters = { id: options.preload };
- createListRequest(this.cid, store, filters, records => {
- // Store obtained records by id.
- const preloaded = {};
- for (const record of records) {
- delete record["_cid"];
- preloaded[record.id] = record;
- }
- runCallback(preloaded);
- });
- }, { mode: "readwrite" });
- return result;
- }
- /**
- * Retrieve a record by its primary key from the IndexedDB database.
- *
- * @override
- * @param {String} id The record id.
- * @return {Promise}
- */
- async get(id) {
- try {
- let record;
- await this.prepare("records", store => {
- store.get([this.cid, id]).onsuccess = e => (record = e.target.result);
- });
- return record;
- }
- catch (e) {
- this._handleError("get", e);
- }
- }
- /**
- * Lists all records from the IndexedDB database.
- *
- * @override
- * @param {Object} params The filters and order to apply to the results.
- * @return {Promise}
- */
- async list(params = { filters: {} }) {
- const { filters } = params;
- try {
- let results = [];
- await this.prepare("records", store => {
- createListRequest(this.cid, store, filters, _results => {
- // we have received all requested records that match the filters,
- // we now park them within current scope and hide the `_cid` attribute.
- for (const result of _results) {
- delete result["_cid"];
- }
- results = _results;
- });
- });
- // The resulting list of records is sorted.
- // XXX: with some efforts, this could be fully implemented using IDB API.
- return params.order ? sortObjects(params.order, results) : results;
- }
- catch (e) {
- this._handleError("list", e);
- }
- }
- /**
- * Store the lastModified value into metadata store.
- *
- * @override
- * @param {Number} lastModified
- * @return {Promise}
- */
- async saveLastModified(lastModified) {
- const value = parseInt(lastModified, 10) || null;
- try {
- await this.prepare("timestamps", store => {
- if (value === null) {
- store.delete(this.cid);
- }
- else {
- store.put({ cid: this.cid, value });
- }
- }, { mode: "readwrite" });
- return value;
- }
- catch (e) {
- this._handleError("saveLastModified", e);
- }
- }
- /**
- * Retrieve saved lastModified value.
- *
- * @override
- * @return {Promise}
- */
- async getLastModified() {
- try {
- let entry = null;
- await this.prepare("timestamps", store => {
- store.get(this.cid).onsuccess = e => (entry = e.target.result);
- });
- return entry ? entry.value : null;
- }
- catch (e) {
- this._handleError("getLastModified", e);
- }
- }
- /**
- * Load a dump of records exported from a server.
- *
- * @deprecated Use {@link importBulk} instead.
- * @abstract
- * @param {Array} records The records to load.
- * @return {Promise}
- */
- async loadDump(records) {
- return this.importBulk(records);
- }
- /**
- * Load records in bulk that were exported from a server.
- *
- * @abstract
- * @param {Array} records The records to load.
- * @return {Promise}
- */
- async importBulk(records) {
- try {
- await this.execute(transaction => {
- // Since the put operations are asynchronous, we chain
- // them together. The last one will be waited for the
- // `transaction.oncomplete` callback. (see #execute())
- let i = 0;
- putNext();
- function putNext() {
- if (i == records.length) {
- return;
- }
- // On error, `transaction.onerror` is called.
- transaction.update(records[i]).onsuccess = putNext;
- ++i;
- }
- });
- const previousLastModified = await this.getLastModified();
- const lastModified = Math.max(...records.map(record => record.last_modified));
- if (lastModified > previousLastModified) {
- await this.saveLastModified(lastModified);
- }
- return records;
- }
- catch (e) {
- this._handleError("importBulk", e);
- }
- }
- async saveMetadata(metadata) {
- try {
- await this.prepare("collections", store => store.put({ cid: this.cid, metadata }), { mode: "readwrite" });
- return metadata;
- }
- catch (e) {
- this._handleError("saveMetadata", e);
- }
- }
- async getMetadata() {
- try {
- let entry = null;
- await this.prepare("collections", store => {
- store.get(this.cid).onsuccess = e => (entry = e.target.result);
- });
- return entry ? entry.metadata : null;
- }
- catch (e) {
- this._handleError("getMetadata", e);
- }
- }
- }
- /**
- * IDB transaction proxy.
- *
- * @param {IDB} adapter The call IDB adapter
- * @param {IDBStore} store The IndexedDB database store.
- * @param {Array} preloaded The list of records to make available to
- * get() (default: []).
- * @return {Object}
- */
- function transactionProxy(adapter, store, preloaded = []) {
- const _cid = adapter.cid;
- return {
- create(record) {
- store.add(Object.assign(Object.assign({}, record), { _cid }));
- },
- update(record) {
- return store.put(Object.assign(Object.assign({}, record), { _cid }));
- },
- delete(id) {
- store.delete([_cid, id]);
- },
- get(id) {
- return preloaded[id];
- },
- };
- }
- /**
- * Up to version 10.X of kinto.js, each collection had its own collection.
- * The database name was `${bid}/${cid}` (eg. `"blocklists/certificates"`)
- * and contained only one store with the same name.
- */
- async function migrationRequired(dbName) {
- let exists = true;
- const db = await open(dbName, {
- version: 1,
- onupgradeneeded: event => {
- exists = false;
- },
- });
- // Check that the DB we're looking at is really a legacy one,
- // and not some remainder of the open() operation above.
- exists &=
- db.objectStoreNames.contains("__meta__") &&
- db.objectStoreNames.contains(dbName);
- if (!exists) {
- db.close();
- // Testing the existence creates it, so delete it :)
- await deleteDatabase(dbName);
- return null;
- }
- console.warn(`${dbName}: old IndexedDB database found.`);
- try {
- // Scan all records.
- let records;
- await execute(db, dbName, store => {
- store.openCursor().onsuccess = cursorHandlers.all({}, res => (records = res));
- });
- console.log(`${dbName}: found ${records.length} records.`);
- // Check if there's a entry for this.
- let timestamp = null;
- await execute(db, "__meta__", store => {
- store.get(`${dbName}-lastModified`).onsuccess = e => {
- timestamp = e.target.result ? e.target.result.value : null;
- };
- });
- // Some previous versions, also used to store the timestamps without prefix.
- if (!timestamp) {
- await execute(db, "__meta__", store => {
- store.get("lastModified").onsuccess = e => {
- timestamp = e.target.result ? e.target.result.value : null;
- };
- });
- }
- console.log(`${dbName}: ${timestamp ? "found" : "no"} timestamp.`);
- // Those will be inserted in the new database/schema.
- return { records, timestamp };
- }
- catch (e) {
- console.error("Error occured during migration", e);
- return null;
- }
- finally {
- db.close();
- }
- }
-
- var uuid4 = {};
-
- const RECORD_FIELDS_TO_CLEAN = ["_status"];
- const AVAILABLE_HOOKS = ["incoming-changes"];
- const IMPORT_CHUNK_SIZE = 200;
- /**
- * Compare two records omitting local fields and synchronization
- * attributes (like _status and last_modified)
- * @param {Object} a A record to compare.
- * @param {Object} b A record to compare.
- * @param {Array} localFields Additional fields to ignore during the comparison
- * @return {boolean}
- */
- function recordsEqual(a, b, localFields = []) {
- const fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields);
- const cleanLocal = r => omitKeys(r, fieldsToClean);
- return deepEqual(cleanLocal(a), cleanLocal(b));
- }
- /**
- * Synchronization result object.
- */
- class SyncResultObject {
- /**
- * Public constructor.
- */
- constructor() {
- /**
- * Current synchronization result status; becomes `false` when conflicts or
- * errors are registered.
- * @type {Boolean}
- */
- this.lastModified = null;
- this._lists = {};
- [
- "errors",
- "created",
- "updated",
- "deleted",
- "published",
- "conflicts",
- "skipped",
- "resolved",
- "void",
- ].forEach(l => (this._lists[l] = []));
- this._cached = {};
- }
- /**
- * Adds entries for a given result type.
- *
- * @param {String} type The result type.
- * @param {Array} entries The result entries.
- * @return {SyncResultObject}
- */
- add(type, entries) {
- if (!Array.isArray(this._lists[type])) {
- console.warn(`Unknown type "${type}"`);
- return;
- }
- if (!Array.isArray(entries)) {
- entries = [entries];
- }
- this._lists[type] = this._lists[type].concat(entries);
- delete this._cached[type];
- return this;
- }
- get ok() {
- return this.errors.length + this.conflicts.length === 0;
- }
- get errors() {
- return this._lists["errors"];
- }
- get conflicts() {
- return this._lists["conflicts"];
- }
- get skipped() {
- return this._deduplicate("skipped");
- }
- get resolved() {
- return this._deduplicate("resolved");
- }
- get created() {
- return this._deduplicate("created");
- }
- get updated() {
- return this._deduplicate("updated");
- }
- get deleted() {
- return this._deduplicate("deleted");
- }
- get published() {
- return this._deduplicate("published");
- }
- _deduplicate(list) {
- if (!(list in this._cached)) {
- // Deduplicate entries by id. If the values don't have `id` attribute, just
- // keep all.
- const recordsWithoutId = new Set();
- const recordsById = new Map();
- this._lists[list].forEach(record => {
- if (!record.id) {
- recordsWithoutId.add(record);
- }
- else {
- recordsById.set(record.id, record);
- }
- });
- this._cached[list] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId));
- }
- return this._cached[list];
- }
- /**
- * Reinitializes result entries for a given result type.
- *
- * @param {String} type The result type.
- * @return {SyncResultObject}
- */
- reset(type) {
- this._lists[type] = [];
- delete this._cached[type];
- return this;
- }
- toObject() {
- // Only used in tests.
- return {
- ok: this.ok,
- lastModified: this.lastModified,
- errors: this.errors,
- created: this.created,
- updated: this.updated,
- deleted: this.deleted,
- skipped: this.skipped,
- published: this.published,
- conflicts: this.conflicts,
- resolved: this.resolved,
- };
- }
- }
- class ServerWasFlushedError extends Error {
- constructor(clientTimestamp, serverTimestamp, message) {
- super(message);
- if (Error.captureStackTrace) {
- Error.captureStackTrace(this, ServerWasFlushedError);
- }
- this.clientTimestamp = clientTimestamp;
- this.serverTimestamp = serverTimestamp;
- }
- }
- function createUUIDSchema() {
- return {
- generate() {
- return uuid4();
- },
- validate(id) {
- return typeof id == "string" && RE_RECORD_ID.test(id);
- },
- };
- }
- function markStatus(record, status) {
- return Object.assign(Object.assign({}, record), { _status: status });
- }
- function markDeleted(record) {
- return markStatus(record, "deleted");
- }
- function markSynced(record) {
- return markStatus(record, "synced");
- }
- /**
- * Import a remote change into the local database.
- *
- * @param {IDBTransactionProxy} transaction The transaction handler.
- * @param {Object} remote The remote change object to import.
- * @param {Array<String>} localFields The list of fields that remain local.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Object}
- */
- function importChange(transaction, remote, localFields, strategy) {
- const local = transaction.get(remote.id);
- if (!local) {
- // Not found locally but remote change is marked as deleted; skip to
- // avoid recreation.
- if (remote.deleted) {
- return { type: "skipped", data: remote };
- }
- const synced = markSynced(remote);
- transaction.create(synced);
- return { type: "created", data: synced };
- }
- // Apply remote changes on local record.
- const synced = Object.assign(Object.assign({}, local), markSynced(remote));
- // With pull only, we don't need to compare records since we override them.
- if (strategy === Collection.strategy.PULL_ONLY) {
- if (remote.deleted) {
- transaction.delete(remote.id);
- return { type: "deleted", data: local };
- }
- transaction.update(synced);
- return { type: "updated", data: { old: local, new: synced } };
- }
- // With other sync strategies, we detect conflicts,
- // by comparing local and remote, ignoring local fields.
- const isIdentical = recordsEqual(local, remote, localFields);
- // Detect or ignore conflicts if record has also been modified locally.
- if (local._status !== "synced") {
- // Locally deleted, unsynced: scheduled for remote deletion.
- if (local._status === "deleted") {
- return { type: "skipped", data: local };
- }
- if (isIdentical) {
- // If records are identical, import anyway, so we bump the
- // local last_modified value from the server and set record
- // status to "synced".
- transaction.update(synced);
- return { type: "updated", data: { old: local, new: synced } };
- }
- if (local.last_modified !== undefined &&
- local.last_modified === remote.last_modified) {
- // If our local version has the same last_modified as the remote
- // one, this represents an object that corresponds to a resolved
- // conflict. Our local version represents the final output, so
- // we keep that one. (No transaction operation to do.)
- // But if our last_modified is undefined,
- // that means we've created the same object locally as one on
- // the server, which *must* be a conflict.
- return { type: "void" };
- }
- return {
- type: "conflicts",
- data: { type: "incoming", local: local, remote: remote },
- };
- }
- // Local record was synced.
- if (remote.deleted) {
- transaction.delete(remote.id);
- return { type: "deleted", data: local };
- }
- // Import locally.
- transaction.update(synced);
- // if identical, simply exclude it from all SyncResultObject lists
- const type = isIdentical ? "void" : "updated";
- return { type, data: { old: local, new: synced } };
- }
- /**
- * Abstracts a collection of records stored in the local database, providing
- * CRUD operations and synchronization helpers.
- */
- class Collection {
- /**
- * Constructor.
- *
- * Options:
- * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
- *
- * @param {String} bucket The bucket identifier.
- * @param {String} name The collection name.
- * @param {KintoBase} kinto The Kinto instance.
- * @param {Object} options The options object.
- */
- constructor(bucket, name, kinto, options = {}) {
- this._bucket = bucket;
- this._name = name;
- this._lastModified = null;
- const DBAdapter = options.adapter || IDB;
- if (!DBAdapter) {
- throw new Error("No adapter provided");
- }
- const db = new DBAdapter(`${bucket}/${name}`, options.adapterOptions);
- if (!(db instanceof BaseAdapter)) {
- throw new Error("Unsupported adapter.");
- }
- // public properties
- /**
- * The db adapter instance
- * @type {BaseAdapter}
- */
- this.db = db;
- /**
- * The KintoBase instance.
- * @type {KintoBase}
- */
- this.kinto = kinto;
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- this.events = options.events;
- /**
- * The IdSchema instance.
- * @type {Object}
- */
- this.idSchema = this._validateIdSchema(options.idSchema);
- /**
- * The list of remote transformers.
- * @type {Array}
- */
- this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers);
- /**
- * The list of hooks.
- * @type {Object}
- */
- this.hooks = this._validateHooks(options.hooks);
- /**
- * The list of fields names that will remain local.
- * @type {Array}
- */
- this.localFields = options.localFields || [];
- }
- /**
- * The HTTP client.
- * @type {KintoClient}
- */
- get api() {
- return this.kinto.api;
- }
- /**
- * The collection name.
- * @type {String}
- */
- get name() {
- return this._name;
- }
- /**
- * The bucket name.
- * @type {String}
- */
- get bucket() {
- return this._bucket;
- }
- /**
- * The last modified timestamp.
- * @type {Number}
- */
- get lastModified() {
- return this._lastModified;
- }
- /**
- * Synchronization strategies. Available strategies are:
- *
- * - `MANUAL`: Conflicts will be reported in a dedicated array.
- * - `SERVER_WINS`: Conflicts are resolved using remote data.
- * - `CLIENT_WINS`: Conflicts are resolved using local data.
- *
- * @type {Object}
- */
- static get strategy() {
- return {
- CLIENT_WINS: "client_wins",
- SERVER_WINS: "server_wins",
- PULL_ONLY: "pull_only",
- MANUAL: "manual",
- };
- }
- /**
- * Validates an idSchema.
- *
- * @param {Object|undefined} idSchema
- * @return {Object}
- */
- _validateIdSchema(idSchema) {
- if (typeof idSchema === "undefined") {
- return createUUIDSchema();
- }
- if (typeof idSchema !== "object") {
- throw new Error("idSchema must be an object.");
- }
- else if (typeof idSchema.generate !== "function") {
- throw new Error("idSchema must provide a generate function.");
- }
- else if (typeof idSchema.validate !== "function") {
- throw new Error("idSchema must provide a validate function.");
- }
- return idSchema;
- }
- /**
- * Validates a list of remote transformers.
- *
- * @param {Array|undefined} remoteTransformers
- * @return {Array}
- */
- _validateRemoteTransformers(remoteTransformers) {
- if (typeof remoteTransformers === "undefined") {
- return [];
- }
- if (!Array.isArray(remoteTransformers)) {
- throw new Error("remoteTransformers should be an array.");
- }
- return remoteTransformers.map(transformer => {
- if (typeof transformer !== "object") {
- throw new Error("A transformer must be an object.");
- }
- else if (typeof transformer.encode !== "function") {
- throw new Error("A transformer must provide an encode function.");
- }
- else if (typeof transformer.decode !== "function") {
- throw new Error("A transformer must provide a decode function.");
- }
- return transformer;
- });
- }
- /**
- * Validate the passed hook is correct.
- *
- * @param {Array|undefined} hook.
- * @return {Array}
- **/
- _validateHook(hook) {
- if (!Array.isArray(hook)) {
- throw new Error("A hook definition should be an array of functions.");
- }
- return hook.map(fn => {
- if (typeof fn !== "function") {
- throw new Error("A hook definition should be an array of functions.");
- }
- return fn;
- });
- }
- /**
- * Validates a list of hooks.
- *
- * @param {Object|undefined} hooks
- * @return {Object}
- */
- _validateHooks(hooks) {
- if (typeof hooks === "undefined") {
- return {};
- }
- if (Array.isArray(hooks)) {
- throw new Error("hooks should be an object, not an array.");
- }
- if (typeof hooks !== "object") {
- throw new Error("hooks should be an object.");
- }
- const validatedHooks = {};
- for (const hook in hooks) {
- if (!AVAILABLE_HOOKS.includes(hook)) {
- throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", "));
- }
- validatedHooks[hook] = this._validateHook(hooks[hook]);
- }
- return validatedHooks;
- }
- /**
- * Deletes every records in the current collection and marks the collection as
- * never synced.
- *
- * @return {Promise}
- */
- async clear() {
- await this.db.clear();
- await this.db.saveMetadata(null);
- await this.db.saveLastModified(null);
- return { data: [], permissions: {} };
- }
- /**
- * Encodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to encode.
- * @return {Promise}
- */
- _encodeRecord(type, record) {
- if (!this[`${type}Transformers`].length) {
- return Promise.resolve(record);
- }
- return waterfall(this[`${type}Transformers`].map(transformer => {
- return record => transformer.encode(record);
- }), record);
- }
- /**
- * Decodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to decode.
- * @return {Promise}
- */
- _decodeRecord(type, record) {
- if (!this[`${type}Transformers`].length) {
- return Promise.resolve(record);
- }
- return waterfall(this[`${type}Transformers`].reverse().map(transformer => {
- return record => transformer.decode(record);
- }), record);
- }
- /**
- * Adds a record to the local database, asserting that none
- * already exist with this ID.
- *
- * Note: If either the `useRecordId` or `synced` options are true, then the
- * record object must contain the id field to be validated. If none of these
- * options are true, an id is generated using the current IdSchema; in this
- * case, the record passed must not have an id.
- *
- * Options:
- * - {Boolean} synced Sets record status to "synced" (default: `false`).
- * - {Boolean} useRecordId Forces the `id` field from the record to be used,
- * instead of one that is generated automatically
- * (default: `false`).
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Promise}
- */
- create(record, options = { useRecordId: false, synced: false }) {
- // Validate the record and its ID (if any), even though this
- // validation is also done in the CollectionTransaction method,
- // because we need to pass the ID to preloadIds.
- const reject = msg => Promise.reject(new Error(msg));
- if (typeof record !== "object") {
- return reject("Record is not an object.");
- }
- if ((options.synced || options.useRecordId) &&
- !Object.prototype.hasOwnProperty.call(record, "id")) {
- return reject("Missing required Id; synced and useRecordId options require one");
- }
- if (!options.synced &&
- !options.useRecordId &&
- Object.prototype.hasOwnProperty.call(record, "id")) {
- return reject("Extraneous Id; can't create a record having one set.");
- }
- const newRecord = Object.assign(Object.assign({}, record), { id: options.synced || options.useRecordId
- ? record.id
- : this.idSchema.generate(record), _status: options.synced ? "synced" : "created" });
- if (!this.idSchema.validate(newRecord.id)) {
- return reject(`Invalid Id: ${newRecord.id}`);
- }
- return this.execute(txn => txn.create(newRecord), {
- preloadIds: [newRecord.id],
- }).catch(err => {
- if (options.useRecordId) {
- throw new Error("Couldn't create record. It may have been virtually deleted.");
- }
- throw err;
- });
- }
- /**
- * Like {@link CollectionTransaction#update}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} synced: Sets record status to "synced" (default: false)
- * - {Boolean} patch: Extends the existing record instead of overwriting it
- * (default: false)
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Promise}
- */
- update(record, options = { synced: false, patch: false }) {
- // Validate the record and its ID, even though this validation is
- // also done in the CollectionTransaction method, because we need
- // to pass the ID to preloadIds.
- if (typeof record !== "object") {
- return Promise.reject(new Error("Record is not an object."));
- }
- if (!Object.prototype.hasOwnProperty.call(record, "id")) {
- return Promise.reject(new Error("Cannot update a record missing id."));
- }
- if (!this.idSchema.validate(record.id)) {
- return Promise.reject(new Error(`Invalid Id: ${record.id}`));
- }
- return this.execute(txn => txn.update(record, options), {
- preloadIds: [record.id],
- });
- }
- /**
- * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
- *
- * @param {Object} record
- * @return {Promise}
- */
- upsert(record) {
- // Validate the record and its ID, even though this validation is
- // also done in the CollectionTransaction method, because we need
- // to pass the ID to preloadIds.
- if (typeof record !== "object") {
- return Promise.reject(new Error("Record is not an object."));
- }
- if (!Object.prototype.hasOwnProperty.call(record, "id")) {
- return Promise.reject(new Error("Cannot update a record missing id."));
- }
- if (!this.idSchema.validate(record.id)) {
- return Promise.reject(new Error(`Invalid Id: ${record.id}`));
- }
- return this.execute(txn => txn.upsert(record), { preloadIds: [record.id] });
- }
- /**
- * Like {@link CollectionTransaction#get}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {String} id
- * @param {Object} options
- * @return {Promise}
- */
- get(id, options = { includeDeleted: false }) {
- return this.execute(txn => txn.get(id, options), { preloadIds: [id] });
- }
- /**
- * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
- *
- * @param {String} id
- * @return {Promise}
- */
- getAny(id) {
- return this.execute(txn => txn.getAny(id), { preloadIds: [id] });
- }
- /**
- * Same as {@link Collection#delete}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
- * update its `_status` attribute to `deleted` instead (default: true)
- *
- * @param {String} id The record's Id.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- delete(id, options = { virtual: true }) {
- return this.execute(transaction => {
- return transaction.delete(id, options);
- }, { preloadIds: [id] });
- }
- /**
- * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter.
- *
- * @return {Promise}
- */
- async deleteAll() {
- const { data } = await this.list({}, { includeDeleted: false });
- const recordIds = data.map(record => record.id);
- return this.execute(transaction => {
- return transaction.deleteAll(recordIds);
- }, { preloadIds: recordIds });
- }
- /**
- * The same as {@link CollectionTransaction#deleteAny}, but wrapped
- * in its own transaction.
- *
- * @param {String} id The record's Id.
- * @return {Promise}
- */
- deleteAny(id) {
- return this.execute(txn => txn.deleteAny(id), { preloadIds: [id] });
- }
- /**
- * Lists records from the local database.
- *
- * Params:
- * - {Object} filters Filter the results (default: `{}`).
- * - {String} order The order to apply (default: `-last_modified`).
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {Object} params The filters and order to apply to the results.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- async list(params = {}, options = { includeDeleted: false }) {
- params = Object.assign({ order: "-last_modified", filters: {} }, params);
- const results = await this.db.list(params);
- let data = results;
- if (!options.includeDeleted) {
- data = results.filter(record => record._status !== "deleted");
- }
- return { data, permissions: {} };
- }
- /**
- * Imports remote changes into the local database.
- * This method is in charge of detecting the conflicts, and resolve them
- * according to the specified strategy.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Array} decodedChanges The list of changes to import in the local database.
- * @param {String} strategy The {@link Collection.strategy} (default: MANUAL)
- * @return {Promise}
- */
- async importChanges(syncResultObject, decodedChanges, strategy = Collection.strategy.MANUAL) {
- // Retrieve records matching change ids.
- try {
- for (let i = 0; i < decodedChanges.length; i += IMPORT_CHUNK_SIZE) {
- const slice = decodedChanges.slice(i, i + IMPORT_CHUNK_SIZE);
- const { imports, resolved } = await this.db.execute(transaction => {
- const imports = slice.map(remote => {
- // Store remote change into local database.
- return importChange(transaction, remote, this.localFields, strategy);
- });
- const conflicts = imports
- .filter(i => i.type === "conflicts")
- .map(i => i.data);
- const resolved = this._handleConflicts(transaction, conflicts, strategy);
- return { imports, resolved };
- }, { preload: slice.map(record => record.id) });
- // Lists of created/updated/deleted records
- imports.forEach(({ type, data }) => syncResultObject.add(type, data));
- // Automatically resolved conflicts (if not manual)
- if (resolved.length > 0) {
- syncResultObject.reset("conflicts").add("resolved", resolved);
- }
- }
- }
- catch (err) {
- const data = {
- type: "incoming",
- message: err.message,
- stack: err.stack,
- };
- // XXX one error of the whole transaction instead of per atomic op
- syncResultObject.add("errors", data);
- }
- return syncResultObject;
- }
- /**
- * Imports the responses of pushed changes into the local database.
- * Basically it stores the timestamp assigned by the server into the local
- * database.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Array} toApplyLocally The list of changes to import in the local database.
- * @param {Array} conflicts The list of conflicts that have to be resolved.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Promise}
- */
- async _applyPushedResults(syncResultObject, toApplyLocally, conflicts, strategy = Collection.strategy.MANUAL) {
- const toDeleteLocally = toApplyLocally.filter(r => r.deleted);
- const toUpdateLocally = toApplyLocally.filter(r => !r.deleted);
- const { published, resolved } = await this.db.execute(transaction => {
- const updated = toUpdateLocally.map(record => {
- const synced = markSynced(record);
- transaction.update(synced);
- return synced;
- });
- const deleted = toDeleteLocally.map(record => {
- transaction.delete(record.id);
- // Amend result data with the deleted attribute set
- return { id: record.id, deleted: true };
- });
- const published = updated.concat(deleted);
- // Handle conflicts, if any
- const resolved = this._handleConflicts(transaction, conflicts, strategy);
- return { published, resolved };
- });
- syncResultObject.add("published", published);
- if (resolved.length > 0) {
- syncResultObject
- .reset("conflicts")
- .reset("resolved")
- .add("resolved", resolved);
- }
- return syncResultObject;
- }
- /**
- * Handles synchronization conflicts according to specified strategy.
- *
- * @param {SyncResultObject} result The sync result object.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Promise<Array<Object>>} The resolved conflicts, as an
- * array of {accepted, rejected} objects
- */
- _handleConflicts(transaction, conflicts, strategy) {
- if (strategy === Collection.strategy.MANUAL) {
- return [];
- }
- return conflicts.map(conflict => {
- const resolution = strategy === Collection.strategy.CLIENT_WINS
- ? conflict.local
- : conflict.remote;
- const rejected = strategy === Collection.strategy.CLIENT_WINS
- ? conflict.remote
- : conflict.local;
- let accepted, status, id;
- if (resolution === null) {
- // We "resolved" with the server-side deletion. Delete locally.
- // This only happens during SERVER_WINS because the local
- // version of a record can never be null.
- // We can get "null" from the remote side if we got a conflict
- // and there is no remote version available; see kinto-http.js
- // batch.js:aggregate.
- transaction.delete(conflict.local.id);
- accepted = null;
- // The record was deleted, but that status is "synced" with
- // the server, so we don't need to push the change.
- status = "synced";
- id = conflict.local.id;
- }
- else {
- const updated = this._resolveRaw(conflict, resolution);
- transaction.update(updated);
- accepted = updated;
- status = updated._status;
- id = updated.id;
- }
- return { rejected, accepted, id, _status: status };
- });
- }
- /**
- * Execute a bunch of operations in a transaction.
- *
- * This transaction should be atomic -- either all of its operations
- * will succeed, or none will.
- *
- * The argument to this function is itself a function which will be
- * called with a {@link CollectionTransaction}. Collection methods
- * are available on this transaction, but instead of returning
- * promises, they are synchronous. execute() returns a Promise whose
- * value will be the return value of the provided function.
- *
- * Most operations will require access to the record itself, which
- * must be preloaded by passing its ID in the preloadIds option.
- *
- * Options:
- * - {Array} preloadIds: list of IDs to fetch at the beginning of
- * the transaction
- *
- * @return {Promise} Resolves with the result of the given function
- * when the transaction commits.
- */
- execute(doOperations, { preloadIds = [] } = {}) {
- for (const id of preloadIds) {
- if (!this.idSchema.validate(id)) {
- return Promise.reject(Error(`Invalid Id: ${id}`));
- }
- }
- return this.db.execute(transaction => {
- const txn = new CollectionTransaction(this, transaction);
- const result = doOperations(txn);
- txn.emitEvents();
- return result;
- }, { preload: preloadIds });
- }
- /**
- * Resets the local records as if they were never synced; existing records are
- * marked as newly created, deleted records are dropped.
- *
- * A next call to {@link Collection.sync} will thus republish the whole
- * content of the local collection to the server.
- *
- * @return {Promise} Resolves with the number of processed records.
- */
- async resetSyncStatus() {
- const unsynced = await this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true });
- await this.db.execute(transaction => {
- unsynced.data.forEach(record => {
- if (record._status === "deleted") {
- // Garbage collect deleted records.
- transaction.delete(record.id);
- }
- else {
- // Records that were synced become «created».
- transaction.update(Object.assign(Object.assign({}, record), { last_modified: undefined, _status: "created" }));
- }
- });
- });
- this._lastModified = null;
- await this.db.saveLastModified(null);
- return unsynced.data.length;
- }
- /**
- * Returns an object containing two lists:
- *
- * - `toDelete`: unsynced deleted records we can safely delete;
- * - `toSync`: local updates to send to the server.
- *
- * @return {Promise}
- */
- async gatherLocalChanges() {
- const unsynced = await this.list({
- filters: { _status: ["created", "updated"] },
- order: "",
- });
- const deleted = await this.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true });
- return await Promise.all(unsynced.data
- .concat(deleted.data)
- .map(this._encodeRecord.bind(this, "remote")));
- }
- /**
- * Fetch remote changes, import them to the local database, and handle
- * conflicts according to `options.strategy`. Then, updates the passed
- * {@link SyncResultObject} with import results.
- *
- * Options:
- * - {String} strategy: The selected sync strategy.
- * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
- * - {Array<String>} exclude: A list of record ids to exclude from pull.
- * - {Object} headers: The HTTP headers to use in the request.
- * - {int} retry: The number of retries to do if the HTTP request fails.
- * - {int} lastModified: The timestamp to use in `?_since` query.
- *
- * @param {KintoClient.Collection} client Kinto client Collection instance.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- async pullChanges(client, syncResultObject, options = {}) {
- if (!syncResultObject.ok) {
- return syncResultObject;
- }
- const since = this.lastModified
- ? this.lastModified
- : await this.db.getLastModified();
- options = Object.assign({ strategy: Collection.strategy.MANUAL, lastModified: since, headers: {} }, options);
- // Optionally ignore some records when pulling for changes.
- // (avoid redownloading our own changes on last step of #sync())
- let filters;
- if (options.exclude) {
- // Limit the list of excluded records to the first 50 records in order
- // to remain under de-facto URL size limit (~2000 chars).
- // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
- const exclude_id = options.exclude
- .slice(0, 50)
- .map(r => r.id)
- .join(",");
- filters = { exclude_id };
- }
- if (options.expectedTimestamp) {
- filters = Object.assign(Object.assign({}, filters), { _expected: options.expectedTimestamp });
- }
- // First fetch remote changes from the server
- const { data, last_modified } = await client.listRecords({
- // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
- since: options.lastModified ? `${options.lastModified}` : undefined,
- headers: options.headers,
- retry: options.retry,
- // Fetch every page by default (FIXME: option to limit pages, see #277)
- pages: Infinity,
- filters,
- });
- // last_modified is the ETag header value (string).
- // For retro-compatibility with first kinto.js versions
- // parse it to integer.
- const unquoted = last_modified ? parseInt(last_modified, 10) : undefined;
- // Check if server was flushed.
- // This is relevant for the Kinto demo server
- // (and thus for many new comers).
- const localSynced = options.lastModified;
- const serverChanged = unquoted > options.lastModified;
- const emptyCollection = data.length === 0;
- if (!options.exclude && localSynced && serverChanged && emptyCollection) {
- const e = new ServerWasFlushedError(localSynced, unquoted, "Server has been flushed. Client Side Timestamp: " +
- localSynced +
- " Server Side Timestamp: " +
- unquoted);
- throw e;
- }
- // Atomic updates are not sensible here because unquoted is not
- // computed as a function of syncResultObject.lastModified.
- // eslint-disable-next-line require-atomic-updates
- syncResultObject.lastModified = unquoted;
- // Decode incoming changes.
- const decodedChanges = await Promise.all(data.map(change => {
- return this._decodeRecord("remote", change);
- }));
- // Hook receives decoded records.
- const payload = { lastModified: unquoted, changes: decodedChanges };
- const afterHooks = await this.applyHook("incoming-changes", payload);
- // No change, nothing to import.
- if (afterHooks.changes.length > 0) {
- // Reflect these changes locally
- await this.importChanges(syncResultObject, afterHooks.changes, options.strategy);
- }
- return syncResultObject;
- }
- applyHook(hookName, payload) {
- if (typeof this.hooks[hookName] == "undefined") {
- return Promise.resolve(payload);
- }
- return waterfall(this.hooks[hookName].map(hook => {
- return record => {
- const result = hook(payload, this);
- const resultThenable = result && typeof result.then === "function";
- const resultChanges = result && Object.prototype.hasOwnProperty.call(result, "changes");
- if (!(resultThenable || resultChanges)) {
- throw new Error(`Invalid return value for hook: ${JSON.stringify(result)} has no 'then()' or 'changes' properties`);
- }
- return result;
- };
- }), payload);
- }
- /**
- * Publish local changes to the remote server and updates the passed
- * {@link SyncResultObject} with publication results.
- *
- * Options:
- * - {String} strategy: The selected sync strategy.
- * - {Object} headers: The HTTP headers to use in the request.
- * - {int} retry: The number of retries to do if the HTTP request fails.
- *
- * @param {KintoClient.Collection} client Kinto client Collection instance.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} changes The change object.
- * @param {Array} changes.toDelete The list of records to delete.
- * @param {Array} changes.toSync The list of records to create/update.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- async pushChanges(client, changes, syncResultObject, options = {}) {
- if (!syncResultObject.ok) {
- return syncResultObject;
- }
- const safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS;
- const toDelete = changes.filter(r => r._status == "deleted");
- const toSync = changes.filter(r => r._status != "deleted");
- // Perform a batch request with every changes.
- const synced = await client.batch(batch => {
- toDelete.forEach(r => {
- // never published locally deleted records should not be pusblished
- if (r.last_modified) {
- batch.deleteRecord(r);
- }
- });
- toSync.forEach(r => {
- // Clean local fields (like _status) before sending to server.
- const published = this.cleanLocalFields(r);
- if (r._status === "created") {
- batch.createRecord(published);
- }
- else {
- batch.updateRecord(published);
- }
- });
- }, {
- headers: options.headers,
- retry: options.retry,
- safe,
- aggregate: true,
- });
- // Store outgoing errors into sync result object
- syncResultObject.add("errors", synced.errors.map(e => (Object.assign(Object.assign({}, e), { type: "outgoing" }))));
- // Store outgoing conflicts into sync result object
- const conflicts = [];
- for (const { type, local, remote } of synced.conflicts) {
- // Note: we ensure that local data are actually available, as they may
- // be missing in the case of a published deletion.
- const safeLocal = (local && local.data) || { id: remote.id };
- const realLocal = await this._decodeRecord("remote", safeLocal);
- // We can get "null" from the remote side if we got a conflict
- // and there is no remote version available; see kinto-http.js
- // batch.js:aggregate.
- const realRemote = remote && (await this._decodeRecord("remote", remote));
- const conflict = { type, local: realLocal, remote: realRemote };
- conflicts.push(conflict);
- }
- syncResultObject.add("conflicts", conflicts);
- // Records that must be deleted are either deletions that were pushed
- // to server (published) or deleted records that were never pushed (skipped).
- const missingRemotely = synced.skipped.map(r => (Object.assign(Object.assign({}, r), { deleted: true })));
- // For created and updated records, the last_modified coming from server
- // will be stored locally.
- // Reflect publication results locally using the response from
- // the batch request.
- const published = synced.published.map(c => c.data);
- const toApplyLocally = published.concat(missingRemotely);
- // Apply the decode transformers, if any
- const decoded = await Promise.all(toApplyLocally.map(record => {
- return this._decodeRecord("remote", record);
- }));
- // We have to update the local records with the responses of the server
- // (eg. last_modified values etc.).
- if (decoded.length > 0 || conflicts.length > 0) {
- await this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);
- }
- return syncResultObject;
- }
- /**
- * Return a copy of the specified record without the local fields.
- *
- * @param {Object} record A record with potential local fields.
- * @return {Object}
- */
- cleanLocalFields(record) {
- const localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields);
- return omitKeys(record, localKeys);
- }
- /**
- * Resolves a conflict, updating local record according to proposed
- * resolution — keeping remote record `last_modified` value as a reference for
- * further batch sending.
- *
- * @param {Object} conflict The conflict object.
- * @param {Object} resolution The proposed record.
- * @return {Promise}
- */
- resolve(conflict, resolution) {
- return this.db.execute(transaction => {
- const updated = this._resolveRaw(conflict, resolution);
- transaction.update(updated);
- return { data: updated, permissions: {} };
- });
- }
- /**
- * @private
- */
- _resolveRaw(conflict, resolution) {
- const resolved = Object.assign(Object.assign({}, resolution), {
- // Ensure local record has the latest authoritative timestamp
- last_modified: conflict.remote && conflict.remote.last_modified });
- // If the resolution object is strictly equal to the
- // remote record, then we can mark it as synced locally.
- // Otherwise, mark it as updated (so that the resolution is pushed).
- const synced = deepEqual(resolved, conflict.remote);
- return markStatus(resolved, synced ? "synced" : "updated");
- }
- /**
- * Synchronize remote and local data. The promise will resolve with a
- * {@link SyncResultObject}, though will reject:
- *
- * - if the server is currently backed off;
- * - if the server has been detected flushed.
- *
- * Options:
- * - {Object} headers: HTTP headers to attach to outgoing requests.
- * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
- * - {Number} retry: Number of retries when server fails to process the request (default: 1).
- * - {Collection.strategy} strategy: See {@link Collection.strategy}.
- * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
- * backed off.
- * - {String} bucket: The remove bucket id to use (default: null)
- * - {String} collection: The remove collection id to use (default: null)
- * - {String} remote The remote Kinto server endpoint to use (default: null).
- *
- * @param {Object} options Options.
- * @return {Promise}
- * @throws {Error} If an invalid remote option is passed.
- */
- async sync(options = {
- strategy: Collection.strategy.MANUAL,
- headers: {},
- retry: 1,
- ignoreBackoff: false,
- bucket: null,
- collection: null,
- remote: null,
- expectedTimestamp: null,
- }) {
- options = Object.assign(Object.assign({}, options), { bucket: options.bucket || this.bucket, collection: options.collection || this.name });
- const previousRemote = this.api.remote;
- if (options.remote) {
- // Note: setting the remote ensures it's valid, throws when invalid.
- this.api.remote = options.remote;
- }
- if (!options.ignoreBackoff && this.api.backoff > 0) {
- const seconds = Math.ceil(this.api.backoff / 1000);
- return Promise.reject(new Error(`Server is asking clients to back off; retry in ${seconds}s or use the ignoreBackoff option.`));
- }
- const client = this.api
- .bucket(options.bucket)
- .collection(options.collection);
- const result = new SyncResultObject();
- try {
- // Fetch collection metadata.
- await this.pullMetadata(client, options);
- // Fetch last changes from the server.
- await this.pullChanges(client, result, options);
- const { lastModified } = result;
- if (options.strategy != Collection.strategy.PULL_ONLY) {
- // Fetch local changes
- const toSync = await this.gatherLocalChanges();
- // Publish local changes and pull local resolutions
- await this.pushChanges(client, toSync, result, options);
- // Publish local resolution of push conflicts to server (on CLIENT_WINS)
- const resolvedUnsynced = result.resolved.filter(r => r._status !== "synced");
- if (resolvedUnsynced.length > 0) {
- const resolvedEncoded = await Promise.all(resolvedUnsynced.map(resolution => {
- let record = resolution.accepted;
- if (record === null) {
- record = { id: resolution.id, _status: resolution._status };
- }
- return this._encodeRecord("remote", record);
- }));
- await this.pushChanges(client, resolvedEncoded, result, options);
- }
- // Perform a last pull to catch changes that occured after the last pull,
- // while local changes were pushed. Do not do it nothing was pushed.
- if (result.published.length > 0) {
- // Avoid redownloading our own changes during the last pull.
- const pullOpts = Object.assign(Object.assign({}, options), { lastModified, exclude: result.published });
- await this.pullChanges(client, result, pullOpts);
- }
- }
- // Don't persist lastModified value if any conflict or error occured
- if (result.ok) {
- // No conflict occured, persist collection's lastModified value
- this._lastModified = await this.db.saveLastModified(result.lastModified);
- }
- }
- catch (e) {
- this.events.emit("sync:error", Object.assign(Object.assign({}, options), { error: e }));
- throw e;
- }
- finally {
- // Ensure API default remote is reverted if a custom one's been used
- this.api.remote = previousRemote;
- }
- this.events.emit("sync:success", Object.assign(Object.assign({}, options), { result }));
- return result;
- }
- /**
- * Load a list of records already synced with the remote server.
- *
- * The local records which are unsynced or whose timestamp is either missing
- * or superior to those being loaded will be ignored.
- *
- * @deprecated Use {@link importBulk} instead.
- * @param {Array} records The previously exported list of records to load.
- * @return {Promise} with the effectively imported records.
- */
- async loadDump(records) {
- return this.importBulk(records);
- }
- /**
- * Load a list of records already synced with the remote server.
- *
- * The local records which are unsynced or whose timestamp is either missing
- * or superior to those being loaded will be ignored.
- *
- * @param {Array} records The previously exported list of records to load.
- * @return {Promise} with the effectively imported records.
- */
- async importBulk(records) {
- if (!Array.isArray(records)) {
- throw new Error("Records is not an array.");
- }
- for (const record of records) {
- if (!Object.prototype.hasOwnProperty.call(record, "id") ||
- !this.idSchema.validate(record.id)) {
- throw new Error("Record has invalid ID: " + JSON.stringify(record));
- }
- if (!record.last_modified) {
- throw new Error("Record has no last_modified value: " + JSON.stringify(record));
- }
- }
- // Fetch all existing records from local database,
- // and skip those who are newer or not marked as synced.
- // XXX filter by status / ids in records
- const { data } = await this.list({}, { includeDeleted: true });
- const existingById = data.reduce((acc, record) => {
- acc[record.id] = record;
- return acc;
- }, {});
- const newRecords = records.filter(record => {
- const localRecord = existingById[record.id];
- const shouldKeep =
- // No local record with this id.
- localRecord === undefined ||
- // Or local record is synced
- (localRecord._status === "synced" &&
- // And was synced from server
- localRecord.last_modified !== undefined &&
- // And is older than imported one.
- record.last_modified > localRecord.last_modified);
- return shouldKeep;
- });
- return await this.db.importBulk(newRecords.map(markSynced));
- }
- async pullMetadata(client, options = {}) {
- const { expectedTimestamp, headers } = options;
- const query = expectedTimestamp
- ? { query: { _expected: expectedTimestamp } }
- : undefined;
- const metadata = await client.getData(Object.assign(Object.assign({}, query), { headers }));
- return this.db.saveMetadata(metadata);
- }
- async metadata() {
- return this.db.getMetadata();
- }
- }
- /**
- * A Collection-oriented wrapper for an adapter's transaction.
- *
- * This defines the high-level functions available on a collection.
- * The collection itself offers functions of the same name. These will
- * perform just one operation in its own transaction.
- */
- class CollectionTransaction {
- constructor(collection, adapterTransaction) {
- this.collection = collection;
- this.adapterTransaction = adapterTransaction;
- this._events = [];
- }
- _queueEvent(action, payload) {
- this._events.push({ action, payload });
- }
- /**
- * Emit queued events, to be called once every transaction operations have
- * been executed successfully.
- */
- emitEvents() {
- for (const { action, payload } of this._events) {
- this.collection.events.emit(action, payload);
- }
- if (this._events.length > 0) {
- const targets = this._events.map(({ action, payload }) => (Object.assign({ action }, payload)));
- this.collection.events.emit("change", { targets });
- }
- this._events = [];
- }
- /**
- * Retrieve a record by its id from the local database, or
- * undefined if none exists.
- *
- * This will also return virtually deleted records.
- *
- * @param {String} id
- * @return {Object}
- */
- getAny(id) {
- const record = this.adapterTransaction.get(id);
- return { data: record, permissions: {} };
- }
- /**
- * Retrieve a record by its id from the local database.
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {String} id
- * @param {Object} options
- * @return {Object}
- */
- get(id, options = { includeDeleted: false }) {
- const res = this.getAny(id);
- if (!res.data ||
- (!options.includeDeleted && res.data._status === "deleted")) {
- throw new Error(`Record with id=${id} not found.`);
- }
- return res;
- }
- /**
- * Deletes a record from the local database.
- *
- * Options:
- * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
- * update its `_status` attribute to `deleted` instead (default: true)
- *
- * @param {String} id The record's Id.
- * @param {Object} options The options object.
- * @return {Object}
- */
- delete(id, options = { virtual: true }) {
- // Ensure the record actually exists.
- const existing = this.adapterTransaction.get(id);
- const alreadyDeleted = existing && existing._status == "deleted";
- if (!existing || (alreadyDeleted && options.virtual)) {
- throw new Error(`Record with id=${id} not found.`);
- }
- // Virtual updates status.
- if (options.virtual) {
- this.adapterTransaction.update(markDeleted(existing));
- }
- else {
- // Delete for real.
- this.adapterTransaction.delete(id);
- }
- this._queueEvent("delete", { data: existing });
- return { data: existing, permissions: {} };
- }
- /**
- * Soft delete all records from the local database.
- *
- * @param {Array} ids Array of non-deleted Record Ids.
- * @return {Object}
- */
- deleteAll(ids) {
- const existingRecords = [];
- ids.forEach(id => {
- existingRecords.push(this.adapterTransaction.get(id));
- this.delete(id);
- });
- this._queueEvent("deleteAll", { data: existingRecords });
- return { data: existingRecords, permissions: {} };
- }
- /**
- * Deletes a record from the local database, if any exists.
- * Otherwise, do nothing.
- *
- * @param {String} id The record's Id.
- * @return {Object}
- */
- deleteAny(id) {
- const existing = this.adapterTransaction.get(id);
- if (existing) {
- this.adapterTransaction.update(markDeleted(existing));
- this._queueEvent("delete", { data: existing });
- }
- return { data: Object.assign({ id }, existing), deleted: !!existing, permissions: {} };
- }
- /**
- * Adds a record to the local database, asserting that none
- * already exist with this ID.
- *
- * @param {Object} record, which must contain an ID
- * @return {Object}
- */
- create(record) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!Object.prototype.hasOwnProperty.call(record, "id")) {
- throw new Error("Cannot create a record missing id");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${record.id}`);
- }
- this.adapterTransaction.create(record);
- this._queueEvent("create", { data: record });
- return { data: record, permissions: {} };
- }
- /**
- * Updates a record from the local database.
- *
- * Options:
- * - {Boolean} synced: Sets record status to "synced" (default: false)
- * - {Boolean} patch: Extends the existing record instead of overwriting it
- * (default: false)
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Object}
- */
- update(record, options = { synced: false, patch: false }) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!Object.prototype.hasOwnProperty.call(record, "id")) {
- throw new Error("Cannot update a record missing id.");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${record.id}`);
- }
- const oldRecord = this.adapterTransaction.get(record.id);
- if (!oldRecord) {
- throw new Error(`Record with id=${record.id} not found.`);
- }
- const newRecord = options.patch ? Object.assign(Object.assign({}, oldRecord), record) : record;
- const updated = this._updateRaw(oldRecord, newRecord, options);
- this.adapterTransaction.update(updated);
- this._queueEvent("update", { data: updated, oldRecord });
- return { data: updated, oldRecord, permissions: {} };
- }
- /**
- * Lower-level primitive for updating a record while respecting
- * _status and last_modified.
- *
- * @param {Object} oldRecord: the record retrieved from the DB
- * @param {Object} newRecord: the record to replace it with
- * @return {Object}
- */
- _updateRaw(oldRecord, newRecord, { synced = false } = {}) {
- const updated = Object.assign({}, newRecord);
- // Make sure to never loose the existing timestamp.
- if (oldRecord && oldRecord.last_modified && !updated.last_modified) {
- updated.last_modified = oldRecord.last_modified;
- }
- // If only local fields have changed, then keep record as synced.
- // If status is created, keep record as created.
- // If status is deleted, mark as updated.
- const isIdentical = oldRecord &&
- recordsEqual(oldRecord, updated, this.collection.localFields);
- const keepSynced = isIdentical && oldRecord._status == "synced";
- const neverSynced = !oldRecord || (oldRecord && oldRecord._status == "created");
- const newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated";
- return markStatus(updated, newStatus);
- }
- /**
- * Upsert a record into the local database.
- *
- * This record must have an ID.
- *
- * If a record with this ID already exists, it will be replaced.
- * Otherwise, this record will be inserted.
- *
- * @param {Object} record
- * @return {Object}
- */
- upsert(record) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!Object.prototype.hasOwnProperty.call(record, "id")) {
- throw new Error("Cannot update a record missing id.");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${record.id}`);
- }
- let oldRecord = this.adapterTransaction.get(record.id);
- const updated = this._updateRaw(oldRecord, record);
- this.adapterTransaction.update(updated);
- // Don't return deleted records -- pretend they are gone
- if (oldRecord && oldRecord._status == "deleted") {
- oldRecord = undefined;
- }
- if (oldRecord) {
- this._queueEvent("update", { data: updated, oldRecord });
- }
- else {
- this._queueEvent("create", { data: updated });
- }
- return { data: updated, oldRecord, permissions: {} };
- }
- }
-
- const DEFAULT_BUCKET_NAME = "default";
- const DEFAULT_REMOTE = "http://localhost:8888/v1";
- const DEFAULT_RETRY = 1;
- /**
- * KintoBase class.
- */
- class KintoBase {
- /**
- * Provides a public access to the base adapter class. Users can create a
- * custom DB adapter by extending {@link BaseAdapter}.
- *
- * @type {Object}
- */
- static get adapters() {
- return {
- BaseAdapter: BaseAdapter,
- };
- }
- /**
- * Synchronization strategies. Available strategies are:
- *
- * - `MANUAL`: Conflicts will be reported in a dedicated array.
- * - `SERVER_WINS`: Conflicts are resolved using remote data.
- * - `CLIENT_WINS`: Conflicts are resolved using local data.
- *
- * @type {Object}
- */
- static get syncStrategy() {
- return Collection.strategy;
- }
- /**
- * Constructor.
- *
- * Options:
- * - `{String}` `remote` The server URL to use.
- * - `{String}` `bucket` The collection bucket name.
- * - `{EventEmitter}` `events` Events handler.
- * - `{BaseAdapter}` `adapter` The base DB adapter class.
- * - `{Object}` `adapterOptions` Options given to the adapter.
- * - `{Object}` `headers` The HTTP headers to use.
- * - `{Object}` `retry` Number of retries when the server fails to process the request (default: `1`)
- * - `{String}` `requestMode` The HTTP CORS mode to use.
- * - `{Number}` `timeout` The requests timeout in ms (default: `5000`).
- *
- * @param {Object} options The options object.
- */
- constructor(options = {}) {
- const defaults = {
- bucket: DEFAULT_BUCKET_NAME,
- remote: DEFAULT_REMOTE,
- retry: DEFAULT_RETRY,
- };
- this._options = Object.assign(Object.assign({}, defaults), options);
- if (!this._options.adapter) {
- throw new Error("No adapter provided");
- }
- this._api = null;
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- this.events = this._options.events;
- }
- /**
- * The kinto HTTP client instance.
- * @type {KintoClient}
- */
- get api() {
- const { events, headers, remote, requestMode, retry, timeout, } = this._options;
- if (!this._api) {
- this._api = new this.ApiClass(remote, {
- events,
- headers,
- requestMode,
- retry,
- timeout,
- });
- }
- return this._api;
- }
- /**
- * Creates a {@link Collection} instance. The second (optional) parameter
- * will set collection-level options like e.g. `remoteTransformers`.
- *
- * @param {String} collName The collection name.
- * @param {Object} [options={}] Extra options or override client's options.
- * @param {Object} [options.idSchema] IdSchema instance (default: UUID)
- * @param {Object} [options.remoteTransformers] Array<RemoteTransformer> (default: `[]`])
- * @param {Object} [options.hooks] Array<Hook> (default: `[]`])
- * @param {Object} [options.localFields] Array<Field> (default: `[]`])
- * @return {Collection}
- */
- collection(collName, options = {}) {
- if (!collName) {
- throw new Error("missing collection name");
- }
- const { bucket, events, adapter, adapterOptions } = Object.assign(Object.assign({}, this._options), options);
- const { idSchema, remoteTransformers, hooks, localFields } = options;
- return new Collection(bucket, collName, this, {
- events,
- adapter,
- adapterOptions,
- idSchema,
- remoteTransformers,
- hooks,
- localFields,
- });
- }
- }
-
- /*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const { setTimeout, clearTimeout } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");
- const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
- XPCOMUtils.defineLazyGlobalGetters(global, ["fetch", "indexedDB"]);
- ChromeUtils.defineESModuleGetters(global, {
- EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
- // Use standalone kinto-http module landed in FFx.
- KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs"
- });
- ChromeUtils.defineLazyGetter(global, "generateUUID", () => {
- const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
- return generateUUID;
- });
- class Kinto extends KintoBase {
- static get adapters() {
- return {
- BaseAdapter,
- IDB,
- };
- }
- get ApiClass() {
- return KintoHttpClient;
- }
- constructor(options = {}) {
- const events = {};
- EventEmitter.decorate(events);
- const defaults = {
- adapter: IDB,
- events,
- };
- super(Object.assign(Object.assign({}, defaults), options));
- }
- collection(collName, options = {}) {
- const idSchema = {
- validate(id) {
- return typeof id == "string" && RE_RECORD_ID.test(id);
- },
- generate() {
- return generateUUID()
- .toString()
- .replace(/[{}]/g, "");
- },
- };
- return super.collection(collName, Object.assign({ idSchema }, options));
- }
- }
-
- return Kinto;
-
-})));
diff --git a/services/common/kinto-offline-client.sys.mjs b/services/common/kinto-offline-client.sys.mjs
new file mode 100644
index 0000000000..3435c57f5a
--- /dev/null
+++ b/services/common/kinto-offline-client.sys.mjs
@@ -0,0 +1,2613 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This file is generated from kinto.js - do not modify directly.
+ */
+
+/*
+ * Version 13.0.0 - 7fbf95d
+ */
+
+/**
+ * Base db adapter.
+ *
+ * @abstract
+ */
+class BaseAdapter {
+ /**
+ * Deletes every records present in the database.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ clear() {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Executes a batch of operations within a single transaction.
+ *
+ * @abstract
+ * @param {Function} callback The operation callback.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ execute(callback, options = { preload: [] }) {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Retrieve a record by its primary key from the database.
+ *
+ * @abstract
+ * @param {String} id The record id.
+ * @return {Promise}
+ */
+ get(id) {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Lists all records from the database.
+ *
+ * @abstract
+ * @param {Object} params The filters and order to apply to the results.
+ * @return {Promise}
+ */
+ list(params = { filters: {}, order: "" }) {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Store the lastModified value.
+ *
+ * @abstract
+ * @param {Number} lastModified
+ * @return {Promise}
+ */
+ saveLastModified(lastModified) {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Retrieve saved lastModified value.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ getLastModified() {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Load records in bulk that were exported from a server.
+ *
+ * @abstract
+ * @param {Array} records The records to load.
+ * @return {Promise}
+ */
+ importBulk(records) {
+ throw new Error("Not Implemented.");
+ }
+ /**
+ * Load a dump of records exported from a server.
+ *
+ * @deprecated Use {@link importBulk} instead.
+ * @abstract
+ * @param {Array} records The records to load.
+ * @return {Promise}
+ */
+ loadDump(records) {
+ throw new Error("Not Implemented.");
+ }
+ saveMetadata(metadata) {
+ throw new Error("Not Implemented.");
+ }
+ getMetadata() {
+ throw new Error("Not Implemented.");
+ }
+}
+
+const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
+/**
+ * Checks if a value is undefined.
+ * @param {Any} value
+ * @return {Boolean}
+ */
+function _isUndefined(value) {
+ return typeof value === "undefined";
+}
+/**
+ * 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}
+ */
+function 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;
+ });
+}
+/**
+ * Test if a single object matches all given filters.
+ *
+ * @param {Object} filters The filters object.
+ * @param {Object} entry The object to filter.
+ * @return {Boolean}
+ */
+function filterObject(filters, entry) {
+ return Object.keys(filters).every(filter => {
+ const value = filters[filter];
+ if (Array.isArray(value)) {
+ return value.some(candidate => candidate === entry[filter]);
+ }
+ else if (typeof value === "object") {
+ return 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;
+ });
+}
+/**
+ * Resolves a list of functions sequentially, which can be sync or async; in
+ * case of async, functions must return a promise.
+ *
+ * @param {Array} fns The list of functions.
+ * @param {Any} init The initial value.
+ * @return {Promise}
+ */
+function waterfall(fns, init) {
+ if (!fns.length) {
+ return Promise.resolve(init);
+ }
+ return fns.reduce((promise, nextFn) => {
+ return promise.then(nextFn);
+ }, Promise.resolve(init));
+}
+/**
+ * Simple deep object comparison function. This only supports comparison of
+ * serializable JavaScript objects.
+ *
+ * @param {Object} a The source object.
+ * @param {Object} b The compared object.
+ * @return {Boolean}
+ */
+function deepEqual(a, b) {
+ if (a === b) {
+ return true;
+ }
+ if (typeof a !== typeof b) {
+ return false;
+ }
+ if (!(a && typeof a == "object") || !(b && typeof b == "object")) {
+ return false;
+ }
+ if (Object.keys(a).length !== Object.keys(b).length) {
+ return false;
+ }
+ for (const k in a) {
+ if (!deepEqual(a[k], b[k])) {
+ return false;
+ }
+ }
+ return true;
+}
+/**
+ * Return an object without the specified keys.
+ *
+ * @param {Object} obj The original object.
+ * @param {Array} keys The list of keys to exclude.
+ * @return {Object} A copy without the specified keys.
+ */
+function omitKeys(obj, keys = []) {
+ const result = Object.assign({}, obj);
+ for (const key of keys) {
+ delete result[key];
+ }
+ return result;
+}
+function arrayEqual(a, b) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = a.length; i--;) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
+ const last = arr.length - 1;
+ return arr.reduce((acc, cv, i) => {
+ if (i === last) {
+ return (acc[cv] = val);
+ }
+ else if (Object.prototype.hasOwnProperty.call(acc, cv)) {
+ return acc[cv];
+ }
+ else {
+ return (acc[cv] = {});
+ }
+ }, nestedFiltersObj);
+}
+function transformSubObjectFilters(filtersObj) {
+ const transformedFilters = {};
+ for (const key in filtersObj) {
+ const keysArr = key.split(".");
+ const val = filtersObj[key];
+ makeNestedObjectFromArr(keysArr, val, transformedFilters);
+ }
+ return transformedFilters;
+}
+
+const INDEXED_FIELDS = ["id", "_status", "last_modified"];
+/**
+ * Small helper that wraps the opening of an IndexedDB into a Promise.
+ *
+ * @param dbname {String} The database name.
+ * @param version {Integer} Schema version
+ * @param onupgradeneeded {Function} The callback to execute if schema is
+ * missing or different.
+ * @return {Promise<IDBDatabase>}
+ */
+async function open(dbname, { version, onupgradeneeded }) {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbname, version);
+ request.onupgradeneeded = event => {
+ const db = event.target.result;
+ db.onerror = event => reject(event.target.error);
+ // When an upgrade is needed, a transaction is started.
+ const transaction = event.target.transaction;
+ transaction.onabort = event => {
+ const error = event.target.error ||
+ transaction.error ||
+ new DOMException("The operation has been aborted", "AbortError");
+ reject(error);
+ };
+ // Callback for store creation etc.
+ return onupgradeneeded(event);
+ };
+ request.onerror = event => {
+ reject(event.target.error);
+ };
+ request.onsuccess = event => {
+ const db = event.target.result;
+ resolve(db);
+ };
+ });
+}
+/**
+ * Helper to run the specified callback in a single transaction on the
+ * specified store.
+ * The helper focuses on transaction wrapping into a promise.
+ *
+ * @param db {IDBDatabase} The database instance.
+ * @param name {String} The store name.
+ * @param callback {Function} The piece of code to execute in the transaction.
+ * @param options {Object} Options.
+ * @param options.mode {String} Transaction mode (default: read).
+ * @return {Promise} any value returned by the callback.
+ */
+async function execute(db, name, callback, options = {}) {
+ const { mode } = options;
+ return new Promise((resolve, reject) => {
+ // On Safari, calling IDBDatabase.transaction with mode == undefined raises
+ // a TypeError.
+ const transaction = mode
+ ? db.transaction([name], mode)
+ : db.transaction([name]);
+ const store = transaction.objectStore(name);
+ // Let the callback abort this transaction.
+ const abort = e => {
+ transaction.abort();
+ reject(e);
+ };
+ // Execute the specified callback **synchronously**.
+ let result;
+ try {
+ result = callback(store, abort);
+ }
+ catch (e) {
+ abort(e);
+ }
+ transaction.onerror = event => reject(event.target.error);
+ transaction.oncomplete = event => resolve(result);
+ transaction.onabort = event => {
+ const error = event.target.error ||
+ transaction.error ||
+ new DOMException("The operation has been aborted", "AbortError");
+ reject(error);
+ };
+ });
+}
+/**
+ * Helper to wrap the deletion of an IndexedDB database into a promise.
+ *
+ * @param dbName {String} the database to delete
+ * @return {Promise}
+ */
+async function deleteDatabase(dbName) {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase(dbName);
+ request.onsuccess = event => resolve(event.target);
+ request.onerror = event => reject(event.target.error);
+ });
+}
+/**
+ * IDB cursor handlers.
+ * @type {Object}
+ */
+const cursorHandlers = {
+ all(filters, done) {
+ const results = [];
+ return event => {
+ const cursor = event.target.result;
+ if (cursor) {
+ const { value } = cursor;
+ if (filterObject(filters, value)) {
+ results.push(value);
+ }
+ cursor.continue();
+ }
+ else {
+ done(results);
+ }
+ };
+ },
+ in(values, filters, done) {
+ const results = [];
+ let i = 0;
+ return function (event) {
+ const cursor = event.target.result;
+ if (!cursor) {
+ done(results);
+ return;
+ }
+ const { key, value } = cursor;
+ // `key` can be an array of two values (see `keyPath` in indices definitions).
+ // `values` can be an array of arrays if we filter using an index whose key path
+ // is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
+ while (key > values[i]) {
+ // The cursor has passed beyond this key. Check next.
+ ++i;
+ if (i === values.length) {
+ done(results); // There is no next. Stop searching.
+ return;
+ }
+ }
+ const isEqual = Array.isArray(key)
+ ? arrayEqual(key, values[i])
+ : key === values[i];
+ if (isEqual) {
+ if (filterObject(filters, value)) {
+ results.push(value);
+ }
+ cursor.continue();
+ }
+ else {
+ cursor.continue(values[i]);
+ }
+ };
+ },
+};
+/**
+ * Creates an IDB request and attach it the appropriate cursor event handler to
+ * perform a list query.
+ *
+ * Multiple matching values are handled by passing an array.
+ *
+ * @param {String} cid The collection id (ie. `{bid}/{cid}`)
+ * @param {IDBStore} store The IDB store.
+ * @param {Object} filters Filter the records by field.
+ * @param {Function} done The operation completion handler.
+ * @return {IDBRequest}
+ */
+function createListRequest(cid, store, filters, done) {
+ const filterFields = Object.keys(filters);
+ // If no filters, get all results in one bulk.
+ if (filterFields.length == 0) {
+ const request = store.index("cid").getAll(IDBKeyRange.only(cid));
+ request.onsuccess = event => done(event.target.result);
+ return request;
+ }
+ // Introspect filters and check if they leverage an indexed field.
+ const indexField = filterFields.find(field => {
+ return INDEXED_FIELDS.includes(field);
+ });
+ if (!indexField) {
+ // Iterate on all records for this collection (ie. cid)
+ const isSubQuery = Object.keys(filters).some(key => key.includes(".")); // (ie. filters: {"article.title": "hello"})
+ if (isSubQuery) {
+ const newFilter = transformSubObjectFilters(filters);
+ const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
+ request.onsuccess = cursorHandlers.all(newFilter, done);
+ return request;
+ }
+ const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
+ request.onsuccess = cursorHandlers.all(filters, done);
+ return request;
+ }
+ // If `indexField` was used already, don't filter again.
+ const remainingFilters = omitKeys(filters, [indexField]);
+ // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
+ const value = filters[indexField];
+ // For the "id" field, use the primary key.
+ const indexStore = indexField == "id" ? store : store.index(indexField);
+ // WHERE IN equivalent clause
+ if (Array.isArray(value)) {
+ if (value.length === 0) {
+ return done([]);
+ }
+ const values = value.map(i => [cid, i]).sort();
+ const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
+ const request = indexStore.openCursor(range);
+ request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
+ return request;
+ }
+ // If no filters on custom attribute, get all results in one bulk.
+ if (remainingFilters.length == 0) {
+ const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
+ request.onsuccess = event => done(event.target.result);
+ return request;
+ }
+ // WHERE field = value clause
+ const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
+ request.onsuccess = cursorHandlers.all(remainingFilters, done);
+ return request;
+}
+class IDBError extends Error {
+ constructor(method, err) {
+ super(`IndexedDB ${method}() ${err.message}`);
+ this.name = err.name;
+ this.stack = err.stack;
+ }
+}
+/**
+ * IndexedDB adapter.
+ *
+ * This adapter doesn't support any options.
+ */
+class IDB extends BaseAdapter {
+ /* Expose the IDBError class publicly */
+ static get IDBError() {
+ return IDBError;
+ }
+ /**
+ * Constructor.
+ *
+ * @param {String} cid The key base for this collection (eg. `bid/cid`)
+ * @param {Object} options
+ * @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`)
+ * @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
+ */
+ constructor(cid, options = {}) {
+ super();
+ this.cid = cid;
+ this.dbName = options.dbName || "KintoDB";
+ this._options = options;
+ this._db = null;
+ }
+ _handleError(method, err) {
+ throw new IDBError(method, err);
+ }
+ /**
+ * Ensures a connection to the IndexedDB database has been opened.
+ *
+ * @override
+ * @return {Promise}
+ */
+ async open() {
+ if (this._db) {
+ return this;
+ }
+ // In previous versions, we used to have a database with name `${bid}/${cid}`.
+ // Check if it exists, and migrate data once new schema is in place.
+ // Note: the built-in migrations from IndexedDB can only be used if the
+ // database name does not change.
+ const dataToMigrate = this._options.migrateOldData
+ ? await migrationRequired(this.cid)
+ : null;
+ this._db = await open(this.dbName, {
+ version: 2,
+ onupgradeneeded: event => {
+ const db = event.target.result;
+ if (event.oldVersion < 1) {
+ // Records store
+ const recordsStore = db.createObjectStore("records", {
+ keyPath: ["_cid", "id"],
+ });
+ // An index to obtain all the records in a collection.
+ recordsStore.createIndex("cid", "_cid");
+ // Here we create indices for every known field in records by collection.
+ // Local record status ("synced", "created", "updated", "deleted")
+ recordsStore.createIndex("_status", ["_cid", "_status"]);
+ // Last modified field
+ recordsStore.createIndex("last_modified", ["_cid", "last_modified"]);
+ // Timestamps store
+ db.createObjectStore("timestamps", {
+ keyPath: "cid",
+ });
+ }
+ if (event.oldVersion < 2) {
+ // Collections store
+ db.createObjectStore("collections", {
+ keyPath: "cid",
+ });
+ }
+ },
+ });
+ if (dataToMigrate) {
+ const { records, timestamp } = dataToMigrate;
+ await this.importBulk(records);
+ await this.saveLastModified(timestamp);
+ console.log(`${this.cid}: data was migrated successfully.`);
+ // Delete the old database.
+ await deleteDatabase(this.cid);
+ console.warn(`${this.cid}: old database was deleted.`);
+ }
+ return this;
+ }
+ /**
+ * Closes current connection to the database.
+ *
+ * @override
+ * @return {Promise}
+ */
+ close() {
+ if (this._db) {
+ this._db.close(); // indexedDB.close is synchronous
+ this._db = null;
+ }
+ return Promise.resolve();
+ }
+ /**
+ * Returns a transaction and an object store for a store name.
+ *
+ * To determine if a transaction has completed successfully, we should rather
+ * listen to the transaction’s complete event rather than the IDBObjectStore
+ * request’s success event, because the transaction may still fail after the
+ * success event fires.
+ *
+ * @param {String} name Store name
+ * @param {Function} callback to execute
+ * @param {Object} options Options
+ * @param {String} options.mode Transaction mode ("readwrite" or undefined)
+ * @return {Object}
+ */
+ async prepare(name, callback, options) {
+ await this.open();
+ await execute(this._db, name, callback, options);
+ }
+ /**
+ * Deletes every records in the current collection.
+ *
+ * @override
+ * @return {Promise}
+ */
+ async clear() {
+ try {
+ await this.prepare("records", store => {
+ const range = IDBKeyRange.only(this.cid);
+ const request = store.index("cid").openKeyCursor(range);
+ request.onsuccess = event => {
+ const cursor = event.target.result;
+ if (cursor) {
+ store.delete(cursor.primaryKey);
+ cursor.continue();
+ }
+ };
+ return request;
+ }, { mode: "readwrite" });
+ }
+ catch (e) {
+ this._handleError("clear", e);
+ }
+ }
+ /**
+ * Executes the set of synchronous CRUD operations described in the provided
+ * callback within an IndexedDB transaction, for current db store.
+ *
+ * The callback will be provided an object exposing the following synchronous
+ * CRUD operation methods: get, create, update, delete.
+ *
+ * Important note: because limitations in IndexedDB implementations, no
+ * asynchronous code should be performed within the provided callback; the
+ * promise will therefore be rejected if the callback returns a Promise.
+ *
+ * Options:
+ * - {Array} preload: The list of record IDs to fetch and make available to
+ * the transaction object get() method (default: [])
+ *
+ * @example
+ * const db = new IDB("example");
+ * const result = await db.execute(transaction => {
+ * transaction.create({id: 1, title: "foo"});
+ * transaction.update({id: 2, title: "bar"});
+ * transaction.delete(3);
+ * return "foo";
+ * });
+ *
+ * @override
+ * @param {Function} callback The operation description callback.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ async execute(callback, options = { preload: [] }) {
+ // Transactions in IndexedDB are autocommited when a callback does not
+ // perform any additional operation.
+ // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
+ // prevents using within an opened transaction.
+ // To avoid managing asynchronocity in the specified `callback`, we preload
+ // a list of record in order to execute the `callback` synchronously.
+ // See also:
+ // - http://stackoverflow.com/a/28388805/330911
+ // - http://stackoverflow.com/a/10405196
+ // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
+ let result;
+ await this.prepare("records", (store, abort) => {
+ const runCallback = (preloaded = []) => {
+ // Expose a consistent API for every adapter instead of raw store methods.
+ const proxy = transactionProxy(this, store, preloaded);
+ // The callback is executed synchronously within the same transaction.
+ try {
+ const returned = callback(proxy);
+ if (returned instanceof Promise) {
+ // XXX: investigate how to provide documentation details in error.
+ throw new Error("execute() callback should not return a Promise.");
+ }
+ // Bring to scope that will be returned (once promise awaited).
+ result = returned;
+ }
+ catch (e) {
+ // The callback has thrown an error explicitly. Abort transaction cleanly.
+ abort(e);
+ }
+ };
+ // No option to preload records, go straight to `callback`.
+ if (!options.preload.length) {
+ return runCallback();
+ }
+ // Preload specified records using a list request.
+ const filters = { id: options.preload };
+ createListRequest(this.cid, store, filters, records => {
+ // Store obtained records by id.
+ const preloaded = {};
+ for (const record of records) {
+ delete record["_cid"];
+ preloaded[record.id] = record;
+ }
+ runCallback(preloaded);
+ });
+ }, { mode: "readwrite" });
+ return result;
+ }
+ /**
+ * Retrieve a record by its primary key from the IndexedDB database.
+ *
+ * @override
+ * @param {String} id The record id.
+ * @return {Promise}
+ */
+ async get(id) {
+ try {
+ let record;
+ await this.prepare("records", store => {
+ store.get([this.cid, id]).onsuccess = e => (record = e.target.result);
+ });
+ return record;
+ }
+ catch (e) {
+ this._handleError("get", e);
+ }
+ }
+ /**
+ * Lists all records from the IndexedDB database.
+ *
+ * @override
+ * @param {Object} params The filters and order to apply to the results.
+ * @return {Promise}
+ */
+ async list(params = { filters: {} }) {
+ const { filters } = params;
+ try {
+ let results = [];
+ await this.prepare("records", store => {
+ createListRequest(this.cid, store, filters, _results => {
+ // we have received all requested records that match the filters,
+ // we now park them within current scope and hide the `_cid` attribute.
+ for (const result of _results) {
+ delete result["_cid"];
+ }
+ results = _results;
+ });
+ });
+ // The resulting list of records is sorted.
+ // XXX: with some efforts, this could be fully implemented using IDB API.
+ return params.order ? sortObjects(params.order, results) : results;
+ }
+ catch (e) {
+ this._handleError("list", e);
+ }
+ }
+ /**
+ * Store the lastModified value into metadata store.
+ *
+ * @override
+ * @param {Number} lastModified
+ * @return {Promise}
+ */
+ async saveLastModified(lastModified) {
+ const value = parseInt(lastModified, 10) || null;
+ try {
+ await this.prepare("timestamps", store => {
+ if (value === null) {
+ store.delete(this.cid);
+ }
+ else {
+ store.put({ cid: this.cid, value });
+ }
+ }, { mode: "readwrite" });
+ return value;
+ }
+ catch (e) {
+ this._handleError("saveLastModified", e);
+ }
+ }
+ /**
+ * Retrieve saved lastModified value.
+ *
+ * @override
+ * @return {Promise}
+ */
+ async getLastModified() {
+ try {
+ let entry = null;
+ await this.prepare("timestamps", store => {
+ store.get(this.cid).onsuccess = e => (entry = e.target.result);
+ });
+ return entry ? entry.value : null;
+ }
+ catch (e) {
+ this._handleError("getLastModified", e);
+ }
+ }
+ /**
+ * Load a dump of records exported from a server.
+ *
+ * @deprecated Use {@link importBulk} instead.
+ * @abstract
+ * @param {Array} records The records to load.
+ * @return {Promise}
+ */
+ async loadDump(records) {
+ return this.importBulk(records);
+ }
+ /**
+ * Load records in bulk that were exported from a server.
+ *
+ * @abstract
+ * @param {Array} records The records to load.
+ * @return {Promise}
+ */
+ async importBulk(records) {
+ try {
+ await this.execute(transaction => {
+ // Since the put operations are asynchronous, we chain
+ // them together. The last one will be waited for the
+ // `transaction.oncomplete` callback. (see #execute())
+ let i = 0;
+ putNext();
+ function putNext() {
+ if (i == records.length) {
+ return;
+ }
+ // On error, `transaction.onerror` is called.
+ transaction.update(records[i]).onsuccess = putNext;
+ ++i;
+ }
+ });
+ const previousLastModified = await this.getLastModified();
+ const lastModified = Math.max(...records.map(record => record.last_modified));
+ if (lastModified > previousLastModified) {
+ await this.saveLastModified(lastModified);
+ }
+ return records;
+ }
+ catch (e) {
+ this._handleError("importBulk", e);
+ }
+ }
+ async saveMetadata(metadata) {
+ try {
+ await this.prepare("collections", store => store.put({ cid: this.cid, metadata }), { mode: "readwrite" });
+ return metadata;
+ }
+ catch (e) {
+ this._handleError("saveMetadata", e);
+ }
+ }
+ async getMetadata() {
+ try {
+ let entry = null;
+ await this.prepare("collections", store => {
+ store.get(this.cid).onsuccess = e => (entry = e.target.result);
+ });
+ return entry ? entry.metadata : null;
+ }
+ catch (e) {
+ this._handleError("getMetadata", e);
+ }
+ }
+}
+/**
+ * IDB transaction proxy.
+ *
+ * @param {IDB} adapter The call IDB adapter
+ * @param {IDBStore} store The IndexedDB database store.
+ * @param {Array} preloaded The list of records to make available to
+ * get() (default: []).
+ * @return {Object}
+ */
+function transactionProxy(adapter, store, preloaded = []) {
+ const _cid = adapter.cid;
+ return {
+ create(record) {
+ store.add(Object.assign(Object.assign({}, record), { _cid }));
+ },
+ update(record) {
+ return store.put(Object.assign(Object.assign({}, record), { _cid }));
+ },
+ delete(id) {
+ store.delete([_cid, id]);
+ },
+ get(id) {
+ return preloaded[id];
+ },
+ };
+}
+/**
+ * Up to version 10.X of kinto.js, each collection had its own collection.
+ * The database name was `${bid}/${cid}` (eg. `"blocklists/certificates"`)
+ * and contained only one store with the same name.
+ */
+async function migrationRequired(dbName) {
+ let exists = true;
+ const db = await open(dbName, {
+ version: 1,
+ onupgradeneeded: event => {
+ exists = false;
+ },
+ });
+ // Check that the DB we're looking at is really a legacy one,
+ // and not some remainder of the open() operation above.
+ exists &=
+ db.objectStoreNames.contains("__meta__") &&
+ db.objectStoreNames.contains(dbName);
+ if (!exists) {
+ db.close();
+ // Testing the existence creates it, so delete it :)
+ await deleteDatabase(dbName);
+ return null;
+ }
+ console.warn(`${dbName}: old IndexedDB database found.`);
+ try {
+ // Scan all records.
+ let records;
+ await execute(db, dbName, store => {
+ store.openCursor().onsuccess = cursorHandlers.all({}, res => (records = res));
+ });
+ console.log(`${dbName}: found ${records.length} records.`);
+ // Check if there's a entry for this.
+ let timestamp = null;
+ await execute(db, "__meta__", store => {
+ store.get(`${dbName}-lastModified`).onsuccess = e => {
+ timestamp = e.target.result ? e.target.result.value : null;
+ };
+ });
+ // Some previous versions, also used to store the timestamps without prefix.
+ if (!timestamp) {
+ await execute(db, "__meta__", store => {
+ store.get("lastModified").onsuccess = e => {
+ timestamp = e.target.result ? e.target.result.value : null;
+ };
+ });
+ }
+ console.log(`${dbName}: ${timestamp ? "found" : "no"} timestamp.`);
+ // Those will be inserted in the new database/schema.
+ return { records, timestamp };
+ }
+ catch (e) {
+ console.error("Error occured during migration", e);
+ return null;
+ }
+ finally {
+ db.close();
+ }
+}
+
+var uuid4 = {};
+
+const RECORD_FIELDS_TO_CLEAN = ["_status"];
+const AVAILABLE_HOOKS = ["incoming-changes"];
+const IMPORT_CHUNK_SIZE = 200;
+/**
+ * Compare two records omitting local fields and synchronization
+ * attributes (like _status and last_modified)
+ * @param {Object} a A record to compare.
+ * @param {Object} b A record to compare.
+ * @param {Array} localFields Additional fields to ignore during the comparison
+ * @return {boolean}
+ */
+function recordsEqual(a, b, localFields = []) {
+ const fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields);
+ const cleanLocal = r => omitKeys(r, fieldsToClean);
+ return deepEqual(cleanLocal(a), cleanLocal(b));
+}
+/**
+ * Synchronization result object.
+ */
+class SyncResultObject {
+ /**
+ * Public constructor.
+ */
+ constructor() {
+ /**
+ * Current synchronization result status; becomes `false` when conflicts or
+ * errors are registered.
+ * @type {Boolean}
+ */
+ this.lastModified = null;
+ this._lists = {};
+ [
+ "errors",
+ "created",
+ "updated",
+ "deleted",
+ "published",
+ "conflicts",
+ "skipped",
+ "resolved",
+ "void",
+ ].forEach(l => (this._lists[l] = []));
+ this._cached = {};
+ }
+ /**
+ * Adds entries for a given result type.
+ *
+ * @param {String} type The result type.
+ * @param {Array} entries The result entries.
+ * @return {SyncResultObject}
+ */
+ add(type, entries) {
+ if (!Array.isArray(this._lists[type])) {
+ console.warn(`Unknown type "${type}"`);
+ return;
+ }
+ if (!Array.isArray(entries)) {
+ entries = [entries];
+ }
+ this._lists[type] = this._lists[type].concat(entries);
+ delete this._cached[type];
+ return this;
+ }
+ get ok() {
+ return this.errors.length + this.conflicts.length === 0;
+ }
+ get errors() {
+ return this._lists["errors"];
+ }
+ get conflicts() {
+ return this._lists["conflicts"];
+ }
+ get skipped() {
+ return this._deduplicate("skipped");
+ }
+ get resolved() {
+ return this._deduplicate("resolved");
+ }
+ get created() {
+ return this._deduplicate("created");
+ }
+ get updated() {
+ return this._deduplicate("updated");
+ }
+ get deleted() {
+ return this._deduplicate("deleted");
+ }
+ get published() {
+ return this._deduplicate("published");
+ }
+ _deduplicate(list) {
+ if (!(list in this._cached)) {
+ // Deduplicate entries by id. If the values don't have `id` attribute, just
+ // keep all.
+ const recordsWithoutId = new Set();
+ const recordsById = new Map();
+ this._lists[list].forEach(record => {
+ if (!record.id) {
+ recordsWithoutId.add(record);
+ }
+ else {
+ recordsById.set(record.id, record);
+ }
+ });
+ this._cached[list] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId));
+ }
+ return this._cached[list];
+ }
+ /**
+ * Reinitializes result entries for a given result type.
+ *
+ * @param {String} type The result type.
+ * @return {SyncResultObject}
+ */
+ reset(type) {
+ this._lists[type] = [];
+ delete this._cached[type];
+ return this;
+ }
+ toObject() {
+ // Only used in tests.
+ return {
+ ok: this.ok,
+ lastModified: this.lastModified,
+ errors: this.errors,
+ created: this.created,
+ updated: this.updated,
+ deleted: this.deleted,
+ skipped: this.skipped,
+ published: this.published,
+ conflicts: this.conflicts,
+ resolved: this.resolved,
+ };
+ }
+}
+class ServerWasFlushedError extends Error {
+ constructor(clientTimestamp, serverTimestamp, message) {
+ super(message);
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, ServerWasFlushedError);
+ }
+ this.clientTimestamp = clientTimestamp;
+ this.serverTimestamp = serverTimestamp;
+ }
+}
+function createUUIDSchema() {
+ return {
+ generate() {
+ return uuid4();
+ },
+ validate(id) {
+ return typeof id == "string" && RE_RECORD_ID.test(id);
+ },
+ };
+}
+function markStatus(record, status) {
+ return Object.assign(Object.assign({}, record), { _status: status });
+}
+function markDeleted(record) {
+ return markStatus(record, "deleted");
+}
+function markSynced(record) {
+ return markStatus(record, "synced");
+}
+/**
+ * Import a remote change into the local database.
+ *
+ * @param {IDBTransactionProxy} transaction The transaction handler.
+ * @param {Object} remote The remote change object to import.
+ * @param {Array<String>} localFields The list of fields that remain local.
+ * @param {String} strategy The {@link Collection.strategy}.
+ * @return {Object}
+ */
+function importChange(transaction, remote, localFields, strategy) {
+ const local = transaction.get(remote.id);
+ if (!local) {
+ // Not found locally but remote change is marked as deleted; skip to
+ // avoid recreation.
+ if (remote.deleted) {
+ return { type: "skipped", data: remote };
+ }
+ const synced = markSynced(remote);
+ transaction.create(synced);
+ return { type: "created", data: synced };
+ }
+ // Apply remote changes on local record.
+ const synced = Object.assign(Object.assign({}, local), markSynced(remote));
+ // With pull only, we don't need to compare records since we override them.
+ if (strategy === Collection.strategy.PULL_ONLY) {
+ if (remote.deleted) {
+ transaction.delete(remote.id);
+ return { type: "deleted", data: local };
+ }
+ transaction.update(synced);
+ return { type: "updated", data: { old: local, new: synced } };
+ }
+ // With other sync strategies, we detect conflicts,
+ // by comparing local and remote, ignoring local fields.
+ const isIdentical = recordsEqual(local, remote, localFields);
+ // Detect or ignore conflicts if record has also been modified locally.
+ if (local._status !== "synced") {
+ // Locally deleted, unsynced: scheduled for remote deletion.
+ if (local._status === "deleted") {
+ return { type: "skipped", data: local };
+ }
+ if (isIdentical) {
+ // If records are identical, import anyway, so we bump the
+ // local last_modified value from the server and set record
+ // status to "synced".
+ transaction.update(synced);
+ return { type: "updated", data: { old: local, new: synced } };
+ }
+ if (local.last_modified !== undefined &&
+ local.last_modified === remote.last_modified) {
+ // If our local version has the same last_modified as the remote
+ // one, this represents an object that corresponds to a resolved
+ // conflict. Our local version represents the final output, so
+ // we keep that one. (No transaction operation to do.)
+ // But if our last_modified is undefined,
+ // that means we've created the same object locally as one on
+ // the server, which *must* be a conflict.
+ return { type: "void" };
+ }
+ return {
+ type: "conflicts",
+ data: { type: "incoming", local: local, remote: remote },
+ };
+ }
+ // Local record was synced.
+ if (remote.deleted) {
+ transaction.delete(remote.id);
+ return { type: "deleted", data: local };
+ }
+ // Import locally.
+ transaction.update(synced);
+ // if identical, simply exclude it from all SyncResultObject lists
+ const type = isIdentical ? "void" : "updated";
+ return { type, data: { old: local, new: synced } };
+}
+/**
+ * Abstracts a collection of records stored in the local database, providing
+ * CRUD operations and synchronization helpers.
+ */
+class Collection {
+ /**
+ * Constructor.
+ *
+ * Options:
+ * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
+ *
+ * @param {String} bucket The bucket identifier.
+ * @param {String} name The collection name.
+ * @param {KintoBase} kinto The Kinto instance.
+ * @param {Object} options The options object.
+ */
+ constructor(bucket, name, kinto, options = {}) {
+ this._bucket = bucket;
+ this._name = name;
+ this._lastModified = null;
+ const DBAdapter = options.adapter || IDB;
+ if (!DBAdapter) {
+ throw new Error("No adapter provided");
+ }
+ const db = new DBAdapter(`${bucket}/${name}`, options.adapterOptions);
+ if (!(db instanceof BaseAdapter)) {
+ throw new Error("Unsupported adapter.");
+ }
+ // public properties
+ /**
+ * The db adapter instance
+ * @type {BaseAdapter}
+ */
+ this.db = db;
+ /**
+ * The KintoBase instance.
+ * @type {KintoBase}
+ */
+ this.kinto = kinto;
+ /**
+ * The event emitter instance.
+ * @type {EventEmitter}
+ */
+ this.events = options.events;
+ /**
+ * The IdSchema instance.
+ * @type {Object}
+ */
+ this.idSchema = this._validateIdSchema(options.idSchema);
+ /**
+ * The list of remote transformers.
+ * @type {Array}
+ */
+ this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers);
+ /**
+ * The list of hooks.
+ * @type {Object}
+ */
+ this.hooks = this._validateHooks(options.hooks);
+ /**
+ * The list of fields names that will remain local.
+ * @type {Array}
+ */
+ this.localFields = options.localFields || [];
+ }
+ /**
+ * The HTTP client.
+ * @type {KintoClient}
+ */
+ get api() {
+ return this.kinto.api;
+ }
+ /**
+ * The collection name.
+ * @type {String}
+ */
+ get name() {
+ return this._name;
+ }
+ /**
+ * The bucket name.
+ * @type {String}
+ */
+ get bucket() {
+ return this._bucket;
+ }
+ /**
+ * The last modified timestamp.
+ * @type {Number}
+ */
+ get lastModified() {
+ return this._lastModified;
+ }
+ /**
+ * Synchronization strategies. Available strategies are:
+ *
+ * - `MANUAL`: Conflicts will be reported in a dedicated array.
+ * - `SERVER_WINS`: Conflicts are resolved using remote data.
+ * - `CLIENT_WINS`: Conflicts are resolved using local data.
+ *
+ * @type {Object}
+ */
+ static get strategy() {
+ return {
+ CLIENT_WINS: "client_wins",
+ SERVER_WINS: "server_wins",
+ PULL_ONLY: "pull_only",
+ MANUAL: "manual",
+ };
+ }
+ /**
+ * Validates an idSchema.
+ *
+ * @param {Object|undefined} idSchema
+ * @return {Object}
+ */
+ _validateIdSchema(idSchema) {
+ if (typeof idSchema === "undefined") {
+ return createUUIDSchema();
+ }
+ if (typeof idSchema !== "object") {
+ throw new Error("idSchema must be an object.");
+ }
+ else if (typeof idSchema.generate !== "function") {
+ throw new Error("idSchema must provide a generate function.");
+ }
+ else if (typeof idSchema.validate !== "function") {
+ throw new Error("idSchema must provide a validate function.");
+ }
+ return idSchema;
+ }
+ /**
+ * Validates a list of remote transformers.
+ *
+ * @param {Array|undefined} remoteTransformers
+ * @return {Array}
+ */
+ _validateRemoteTransformers(remoteTransformers) {
+ if (typeof remoteTransformers === "undefined") {
+ return [];
+ }
+ if (!Array.isArray(remoteTransformers)) {
+ throw new Error("remoteTransformers should be an array.");
+ }
+ return remoteTransformers.map(transformer => {
+ if (typeof transformer !== "object") {
+ throw new Error("A transformer must be an object.");
+ }
+ else if (typeof transformer.encode !== "function") {
+ throw new Error("A transformer must provide an encode function.");
+ }
+ else if (typeof transformer.decode !== "function") {
+ throw new Error("A transformer must provide a decode function.");
+ }
+ return transformer;
+ });
+ }
+ /**
+ * Validate the passed hook is correct.
+ *
+ * @param {Array|undefined} hook.
+ * @return {Array}
+ **/
+ _validateHook(hook) {
+ if (!Array.isArray(hook)) {
+ throw new Error("A hook definition should be an array of functions.");
+ }
+ return hook.map(fn => {
+ if (typeof fn !== "function") {
+ throw new Error("A hook definition should be an array of functions.");
+ }
+ return fn;
+ });
+ }
+ /**
+ * Validates a list of hooks.
+ *
+ * @param {Object|undefined} hooks
+ * @return {Object}
+ */
+ _validateHooks(hooks) {
+ if (typeof hooks === "undefined") {
+ return {};
+ }
+ if (Array.isArray(hooks)) {
+ throw new Error("hooks should be an object, not an array.");
+ }
+ if (typeof hooks !== "object") {
+ throw new Error("hooks should be an object.");
+ }
+ const validatedHooks = {};
+ for (const hook in hooks) {
+ if (!AVAILABLE_HOOKS.includes(hook)) {
+ throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", "));
+ }
+ validatedHooks[hook] = this._validateHook(hooks[hook]);
+ }
+ return validatedHooks;
+ }
+ /**
+ * Deletes every records in the current collection and marks the collection as
+ * never synced.
+ *
+ * @return {Promise}
+ */
+ async clear() {
+ await this.db.clear();
+ await this.db.saveMetadata(null);
+ await this.db.saveLastModified(null);
+ return { data: [], permissions: {} };
+ }
+ /**
+ * Encodes a record.
+ *
+ * @param {String} type Either "remote" or "local".
+ * @param {Object} record The record object to encode.
+ * @return {Promise}
+ */
+ _encodeRecord(type, record) {
+ if (!this[`${type}Transformers`].length) {
+ return Promise.resolve(record);
+ }
+ return waterfall(this[`${type}Transformers`].map(transformer => {
+ return record => transformer.encode(record);
+ }), record);
+ }
+ /**
+ * Decodes a record.
+ *
+ * @param {String} type Either "remote" or "local".
+ * @param {Object} record The record object to decode.
+ * @return {Promise}
+ */
+ _decodeRecord(type, record) {
+ if (!this[`${type}Transformers`].length) {
+ return Promise.resolve(record);
+ }
+ return waterfall(this[`${type}Transformers`].reverse().map(transformer => {
+ return record => transformer.decode(record);
+ }), record);
+ }
+ /**
+ * Adds a record to the local database, asserting that none
+ * already exist with this ID.
+ *
+ * Note: If either the `useRecordId` or `synced` options are true, then the
+ * record object must contain the id field to be validated. If none of these
+ * options are true, an id is generated using the current IdSchema; in this
+ * case, the record passed must not have an id.
+ *
+ * Options:
+ * - {Boolean} synced Sets record status to "synced" (default: `false`).
+ * - {Boolean} useRecordId Forces the `id` field from the record to be used,
+ * instead of one that is generated automatically
+ * (default: `false`).
+ *
+ * @param {Object} record
+ * @param {Object} options
+ * @return {Promise}
+ */
+ create(record, options = { useRecordId: false, synced: false }) {
+ // Validate the record and its ID (if any), even though this
+ // validation is also done in the CollectionTransaction method,
+ // because we need to pass the ID to preloadIds.
+ const reject = msg => Promise.reject(new Error(msg));
+ if (typeof record !== "object") {
+ return reject("Record is not an object.");
+ }
+ if ((options.synced || options.useRecordId) &&
+ !Object.prototype.hasOwnProperty.call(record, "id")) {
+ return reject("Missing required Id; synced and useRecordId options require one");
+ }
+ if (!options.synced &&
+ !options.useRecordId &&
+ Object.prototype.hasOwnProperty.call(record, "id")) {
+ return reject("Extraneous Id; can't create a record having one set.");
+ }
+ const newRecord = Object.assign(Object.assign({}, record), { id: options.synced || options.useRecordId
+ ? record.id
+ : this.idSchema.generate(record), _status: options.synced ? "synced" : "created" });
+ if (!this.idSchema.validate(newRecord.id)) {
+ return reject(`Invalid Id: ${newRecord.id}`);
+ }
+ return this.execute(txn => txn.create(newRecord), {
+ preloadIds: [newRecord.id],
+ }).catch(err => {
+ if (options.useRecordId) {
+ throw new Error("Couldn't create record. It may have been virtually deleted.");
+ }
+ throw err;
+ });
+ }
+ /**
+ * Like {@link CollectionTransaction#update}, but wrapped in its own transaction.
+ *
+ * Options:
+ * - {Boolean} synced: Sets record status to "synced" (default: false)
+ * - {Boolean} patch: Extends the existing record instead of overwriting it
+ * (default: false)
+ *
+ * @param {Object} record
+ * @param {Object} options
+ * @return {Promise}
+ */
+ update(record, options = { synced: false, patch: false }) {
+ // Validate the record and its ID, even though this validation is
+ // also done in the CollectionTransaction method, because we need
+ // to pass the ID to preloadIds.
+ if (typeof record !== "object") {
+ return Promise.reject(new Error("Record is not an object."));
+ }
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) {
+ return Promise.reject(new Error("Cannot update a record missing id."));
+ }
+ if (!this.idSchema.validate(record.id)) {
+ return Promise.reject(new Error(`Invalid Id: ${record.id}`));
+ }
+ return this.execute(txn => txn.update(record, options), {
+ preloadIds: [record.id],
+ });
+ }
+ /**
+ * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
+ *
+ * @param {Object} record
+ * @return {Promise}
+ */
+ upsert(record) {
+ // Validate the record and its ID, even though this validation is
+ // also done in the CollectionTransaction method, because we need
+ // to pass the ID to preloadIds.
+ if (typeof record !== "object") {
+ return Promise.reject(new Error("Record is not an object."));
+ }
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) {
+ return Promise.reject(new Error("Cannot update a record missing id."));
+ }
+ if (!this.idSchema.validate(record.id)) {
+ return Promise.reject(new Error(`Invalid Id: ${record.id}`));
+ }
+ return this.execute(txn => txn.upsert(record), { preloadIds: [record.id] });
+ }
+ /**
+ * Like {@link CollectionTransaction#get}, but wrapped in its own transaction.
+ *
+ * Options:
+ * - {Boolean} includeDeleted: Include virtually deleted records.
+ *
+ * @param {String} id
+ * @param {Object} options
+ * @return {Promise}
+ */
+ get(id, options = { includeDeleted: false }) {
+ return this.execute(txn => txn.get(id, options), { preloadIds: [id] });
+ }
+ /**
+ * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
+ *
+ * @param {String} id
+ * @return {Promise}
+ */
+ getAny(id) {
+ return this.execute(txn => txn.getAny(id), { preloadIds: [id] });
+ }
+ /**
+ * Same as {@link Collection#delete}, but wrapped in its own transaction.
+ *
+ * Options:
+ * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
+ * update its `_status` attribute to `deleted` instead (default: true)
+ *
+ * @param {String} id The record's Id.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ delete(id, options = { virtual: true }) {
+ return this.execute(transaction => {
+ return transaction.delete(id, options);
+ }, { preloadIds: [id] });
+ }
+ /**
+ * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter.
+ *
+ * @return {Promise}
+ */
+ async deleteAll() {
+ const { data } = await this.list({}, { includeDeleted: false });
+ const recordIds = data.map(record => record.id);
+ return this.execute(transaction => {
+ return transaction.deleteAll(recordIds);
+ }, { preloadIds: recordIds });
+ }
+ /**
+ * The same as {@link CollectionTransaction#deleteAny}, but wrapped
+ * in its own transaction.
+ *
+ * @param {String} id The record's Id.
+ * @return {Promise}
+ */
+ deleteAny(id) {
+ return this.execute(txn => txn.deleteAny(id), { preloadIds: [id] });
+ }
+ /**
+ * Lists records from the local database.
+ *
+ * Params:
+ * - {Object} filters Filter the results (default: `{}`).
+ * - {String} order The order to apply (default: `-last_modified`).
+ *
+ * Options:
+ * - {Boolean} includeDeleted: Include virtually deleted records.
+ *
+ * @param {Object} params The filters and order to apply to the results.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ async list(params = {}, options = { includeDeleted: false }) {
+ params = Object.assign({ order: "-last_modified", filters: {} }, params);
+ const results = await this.db.list(params);
+ let data = results;
+ if (!options.includeDeleted) {
+ data = results.filter(record => record._status !== "deleted");
+ }
+ return { data, permissions: {} };
+ }
+ /**
+ * Imports remote changes into the local database.
+ * This method is in charge of detecting the conflicts, and resolve them
+ * according to the specified strategy.
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Array} decodedChanges The list of changes to import in the local database.
+ * @param {String} strategy The {@link Collection.strategy} (default: MANUAL)
+ * @return {Promise}
+ */
+ async importChanges(syncResultObject, decodedChanges, strategy = Collection.strategy.MANUAL) {
+ // Retrieve records matching change ids.
+ try {
+ for (let i = 0; i < decodedChanges.length; i += IMPORT_CHUNK_SIZE) {
+ const slice = decodedChanges.slice(i, i + IMPORT_CHUNK_SIZE);
+ const { imports, resolved } = await this.db.execute(transaction => {
+ const imports = slice.map(remote => {
+ // Store remote change into local database.
+ return importChange(transaction, remote, this.localFields, strategy);
+ });
+ const conflicts = imports
+ .filter(i => i.type === "conflicts")
+ .map(i => i.data);
+ const resolved = this._handleConflicts(transaction, conflicts, strategy);
+ return { imports, resolved };
+ }, { preload: slice.map(record => record.id) });
+ // Lists of created/updated/deleted records
+ imports.forEach(({ type, data }) => syncResultObject.add(type, data));
+ // Automatically resolved conflicts (if not manual)
+ if (resolved.length > 0) {
+ syncResultObject.reset("conflicts").add("resolved", resolved);
+ }
+ }
+ }
+ catch (err) {
+ const data = {
+ type: "incoming",
+ message: err.message,
+ stack: err.stack,
+ };
+ // XXX one error of the whole transaction instead of per atomic op
+ syncResultObject.add("errors", data);
+ }
+ return syncResultObject;
+ }
+ /**
+ * Imports the responses of pushed changes into the local database.
+ * Basically it stores the timestamp assigned by the server into the local
+ * database.
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Array} toApplyLocally The list of changes to import in the local database.
+ * @param {Array} conflicts The list of conflicts that have to be resolved.
+ * @param {String} strategy The {@link Collection.strategy}.
+ * @return {Promise}
+ */
+ async _applyPushedResults(syncResultObject, toApplyLocally, conflicts, strategy = Collection.strategy.MANUAL) {
+ const toDeleteLocally = toApplyLocally.filter(r => r.deleted);
+ const toUpdateLocally = toApplyLocally.filter(r => !r.deleted);
+ const { published, resolved } = await this.db.execute(transaction => {
+ const updated = toUpdateLocally.map(record => {
+ const synced = markSynced(record);
+ transaction.update(synced);
+ return synced;
+ });
+ const deleted = toDeleteLocally.map(record => {
+ transaction.delete(record.id);
+ // Amend result data with the deleted attribute set
+ return { id: record.id, deleted: true };
+ });
+ const published = updated.concat(deleted);
+ // Handle conflicts, if any
+ const resolved = this._handleConflicts(transaction, conflicts, strategy);
+ return { published, resolved };
+ });
+ syncResultObject.add("published", published);
+ if (resolved.length > 0) {
+ syncResultObject
+ .reset("conflicts")
+ .reset("resolved")
+ .add("resolved", resolved);
+ }
+ return syncResultObject;
+ }
+ /**
+ * Handles synchronization conflicts according to specified strategy.
+ *
+ * @param {SyncResultObject} result The sync result object.
+ * @param {String} strategy The {@link Collection.strategy}.
+ * @return {Promise<Array<Object>>} The resolved conflicts, as an
+ * array of {accepted, rejected} objects
+ */
+ _handleConflicts(transaction, conflicts, strategy) {
+ if (strategy === Collection.strategy.MANUAL) {
+ return [];
+ }
+ return conflicts.map(conflict => {
+ const resolution = strategy === Collection.strategy.CLIENT_WINS
+ ? conflict.local
+ : conflict.remote;
+ const rejected = strategy === Collection.strategy.CLIENT_WINS
+ ? conflict.remote
+ : conflict.local;
+ let accepted, status, id;
+ if (resolution === null) {
+ // We "resolved" with the server-side deletion. Delete locally.
+ // This only happens during SERVER_WINS because the local
+ // version of a record can never be null.
+ // We can get "null" from the remote side if we got a conflict
+ // and there is no remote version available; see kinto-http.js
+ // batch.js:aggregate.
+ transaction.delete(conflict.local.id);
+ accepted = null;
+ // The record was deleted, but that status is "synced" with
+ // the server, so we don't need to push the change.
+ status = "synced";
+ id = conflict.local.id;
+ }
+ else {
+ const updated = this._resolveRaw(conflict, resolution);
+ transaction.update(updated);
+ accepted = updated;
+ status = updated._status;
+ id = updated.id;
+ }
+ return { rejected, accepted, id, _status: status };
+ });
+ }
+ /**
+ * Execute a bunch of operations in a transaction.
+ *
+ * This transaction should be atomic -- either all of its operations
+ * will succeed, or none will.
+ *
+ * The argument to this function is itself a function which will be
+ * called with a {@link CollectionTransaction}. Collection methods
+ * are available on this transaction, but instead of returning
+ * promises, they are synchronous. execute() returns a Promise whose
+ * value will be the return value of the provided function.
+ *
+ * Most operations will require access to the record itself, which
+ * must be preloaded by passing its ID in the preloadIds option.
+ *
+ * Options:
+ * - {Array} preloadIds: list of IDs to fetch at the beginning of
+ * the transaction
+ *
+ * @return {Promise} Resolves with the result of the given function
+ * when the transaction commits.
+ */
+ execute(doOperations, { preloadIds = [] } = {}) {
+ for (const id of preloadIds) {
+ if (!this.idSchema.validate(id)) {
+ return Promise.reject(Error(`Invalid Id: ${id}`));
+ }
+ }
+ return this.db.execute(transaction => {
+ const txn = new CollectionTransaction(this, transaction);
+ const result = doOperations(txn);
+ txn.emitEvents();
+ return result;
+ }, { preload: preloadIds });
+ }
+ /**
+ * Resets the local records as if they were never synced; existing records are
+ * marked as newly created, deleted records are dropped.
+ *
+ * A next call to {@link Collection.sync} will thus republish the whole
+ * content of the local collection to the server.
+ *
+ * @return {Promise} Resolves with the number of processed records.
+ */
+ async resetSyncStatus() {
+ const unsynced = await this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true });
+ await this.db.execute(transaction => {
+ unsynced.data.forEach(record => {
+ if (record._status === "deleted") {
+ // Garbage collect deleted records.
+ transaction.delete(record.id);
+ }
+ else {
+ // Records that were synced become «created».
+ transaction.update(Object.assign(Object.assign({}, record), { last_modified: undefined, _status: "created" }));
+ }
+ });
+ });
+ this._lastModified = null;
+ await this.db.saveLastModified(null);
+ return unsynced.data.length;
+ }
+ /**
+ * Returns an object containing two lists:
+ *
+ * - `toDelete`: unsynced deleted records we can safely delete;
+ * - `toSync`: local updates to send to the server.
+ *
+ * @return {Promise}
+ */
+ async gatherLocalChanges() {
+ const unsynced = await this.list({
+ filters: { _status: ["created", "updated"] },
+ order: "",
+ });
+ const deleted = await this.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true });
+ return await Promise.all(unsynced.data
+ .concat(deleted.data)
+ .map(this._encodeRecord.bind(this, "remote")));
+ }
+ /**
+ * Fetch remote changes, import them to the local database, and handle
+ * conflicts according to `options.strategy`. Then, updates the passed
+ * {@link SyncResultObject} with import results.
+ *
+ * Options:
+ * - {String} strategy: The selected sync strategy.
+ * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
+ * - {Array<String>} exclude: A list of record ids to exclude from pull.
+ * - {Object} headers: The HTTP headers to use in the request.
+ * - {int} retry: The number of retries to do if the HTTP request fails.
+ * - {int} lastModified: The timestamp to use in `?_since` query.
+ *
+ * @param {KintoClient.Collection} client Kinto client Collection instance.
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ async pullChanges(client, syncResultObject, options = {}) {
+ if (!syncResultObject.ok) {
+ return syncResultObject;
+ }
+ const since = this.lastModified
+ ? this.lastModified
+ : await this.db.getLastModified();
+ options = Object.assign({ strategy: Collection.strategy.MANUAL, lastModified: since, headers: {} }, options);
+ // Optionally ignore some records when pulling for changes.
+ // (avoid redownloading our own changes on last step of #sync())
+ let filters;
+ if (options.exclude) {
+ // Limit the list of excluded records to the first 50 records in order
+ // to remain under de-facto URL size limit (~2000 chars).
+ // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
+ const exclude_id = options.exclude
+ .slice(0, 50)
+ .map(r => r.id)
+ .join(",");
+ filters = { exclude_id };
+ }
+ if (options.expectedTimestamp) {
+ filters = Object.assign(Object.assign({}, filters), { _expected: options.expectedTimestamp });
+ }
+ // First fetch remote changes from the server
+ const { data, last_modified } = await client.listRecords({
+ // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
+ since: options.lastModified ? `${options.lastModified}` : undefined,
+ headers: options.headers,
+ retry: options.retry,
+ // Fetch every page by default (FIXME: option to limit pages, see #277)
+ pages: Infinity,
+ filters,
+ });
+ // last_modified is the ETag header value (string).
+ // For retro-compatibility with first kinto.js versions
+ // parse it to integer.
+ const unquoted = last_modified ? parseInt(last_modified, 10) : undefined;
+ // Check if server was flushed.
+ // This is relevant for the Kinto demo server
+ // (and thus for many new comers).
+ const localSynced = options.lastModified;
+ const serverChanged = unquoted > options.lastModified;
+ const emptyCollection = data.length === 0;
+ if (!options.exclude && localSynced && serverChanged && emptyCollection) {
+ const e = new ServerWasFlushedError(localSynced, unquoted, "Server has been flushed. Client Side Timestamp: " +
+ localSynced +
+ " Server Side Timestamp: " +
+ unquoted);
+ throw e;
+ }
+ // Atomic updates are not sensible here because unquoted is not
+ // computed as a function of syncResultObject.lastModified.
+ // eslint-disable-next-line require-atomic-updates
+ syncResultObject.lastModified = unquoted;
+ // Decode incoming changes.
+ const decodedChanges = await Promise.all(data.map(change => {
+ return this._decodeRecord("remote", change);
+ }));
+ // Hook receives decoded records.
+ const payload = { lastModified: unquoted, changes: decodedChanges };
+ const afterHooks = await this.applyHook("incoming-changes", payload);
+ // No change, nothing to import.
+ if (afterHooks.changes.length > 0) {
+ // Reflect these changes locally
+ await this.importChanges(syncResultObject, afterHooks.changes, options.strategy);
+ }
+ return syncResultObject;
+ }
+ applyHook(hookName, payload) {
+ if (typeof this.hooks[hookName] == "undefined") {
+ return Promise.resolve(payload);
+ }
+ return waterfall(this.hooks[hookName].map(hook => {
+ return record => {
+ const result = hook(payload, this);
+ const resultThenable = result && typeof result.then === "function";
+ const resultChanges = result && Object.prototype.hasOwnProperty.call(result, "changes");
+ if (!(resultThenable || resultChanges)) {
+ throw new Error(`Invalid return value for hook: ${JSON.stringify(result)} has no 'then()' or 'changes' properties`);
+ }
+ return result;
+ };
+ }), payload);
+ }
+ /**
+ * Publish local changes to the remote server and updates the passed
+ * {@link SyncResultObject} with publication results.
+ *
+ * Options:
+ * - {String} strategy: The selected sync strategy.
+ * - {Object} headers: The HTTP headers to use in the request.
+ * - {int} retry: The number of retries to do if the HTTP request fails.
+ *
+ * @param {KintoClient.Collection} client Kinto client Collection instance.
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Object} changes The change object.
+ * @param {Array} changes.toDelete The list of records to delete.
+ * @param {Array} changes.toSync The list of records to create/update.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ async pushChanges(client, changes, syncResultObject, options = {}) {
+ if (!syncResultObject.ok) {
+ return syncResultObject;
+ }
+ const safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS;
+ const toDelete = changes.filter(r => r._status == "deleted");
+ const toSync = changes.filter(r => r._status != "deleted");
+ // Perform a batch request with every changes.
+ const synced = await client.batch(batch => {
+ toDelete.forEach(r => {
+ // never published locally deleted records should not be pusblished
+ if (r.last_modified) {
+ batch.deleteRecord(r);
+ }
+ });
+ toSync.forEach(r => {
+ // Clean local fields (like _status) before sending to server.
+ const published = this.cleanLocalFields(r);
+ if (r._status === "created") {
+ batch.createRecord(published);
+ }
+ else {
+ batch.updateRecord(published);
+ }
+ });
+ }, {
+ headers: options.headers,
+ retry: options.retry,
+ safe,
+ aggregate: true,
+ });
+ // Store outgoing errors into sync result object
+ syncResultObject.add("errors", synced.errors.map(e => (Object.assign(Object.assign({}, e), { type: "outgoing" }))));
+ // Store outgoing conflicts into sync result object
+ const conflicts = [];
+ for (const { type, local, remote } of synced.conflicts) {
+ // Note: we ensure that local data are actually available, as they may
+ // be missing in the case of a published deletion.
+ const safeLocal = (local && local.data) || { id: remote.id };
+ const realLocal = await this._decodeRecord("remote", safeLocal);
+ // We can get "null" from the remote side if we got a conflict
+ // and there is no remote version available; see kinto-http.js
+ // batch.js:aggregate.
+ const realRemote = remote && (await this._decodeRecord("remote", remote));
+ const conflict = { type, local: realLocal, remote: realRemote };
+ conflicts.push(conflict);
+ }
+ syncResultObject.add("conflicts", conflicts);
+ // Records that must be deleted are either deletions that were pushed
+ // to server (published) or deleted records that were never pushed (skipped).
+ const missingRemotely = synced.skipped.map(r => (Object.assign(Object.assign({}, r), { deleted: true })));
+ // For created and updated records, the last_modified coming from server
+ // will be stored locally.
+ // Reflect publication results locally using the response from
+ // the batch request.
+ const published = synced.published.map(c => c.data);
+ const toApplyLocally = published.concat(missingRemotely);
+ // Apply the decode transformers, if any
+ const decoded = await Promise.all(toApplyLocally.map(record => {
+ return this._decodeRecord("remote", record);
+ }));
+ // We have to update the local records with the responses of the server
+ // (eg. last_modified values etc.).
+ if (decoded.length > 0 || conflicts.length > 0) {
+ await this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);
+ }
+ return syncResultObject;
+ }
+ /**
+ * Return a copy of the specified record without the local fields.
+ *
+ * @param {Object} record A record with potential local fields.
+ * @return {Object}
+ */
+ cleanLocalFields(record) {
+ const localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields);
+ return omitKeys(record, localKeys);
+ }
+ /**
+ * Resolves a conflict, updating local record according to proposed
+ * resolution — keeping remote record `last_modified` value as a reference for
+ * further batch sending.
+ *
+ * @param {Object} conflict The conflict object.
+ * @param {Object} resolution The proposed record.
+ * @return {Promise}
+ */
+ resolve(conflict, resolution) {
+ return this.db.execute(transaction => {
+ const updated = this._resolveRaw(conflict, resolution);
+ transaction.update(updated);
+ return { data: updated, permissions: {} };
+ });
+ }
+ /**
+ * @private
+ */
+ _resolveRaw(conflict, resolution) {
+ const resolved = Object.assign(Object.assign({}, resolution), {
+ // Ensure local record has the latest authoritative timestamp
+ last_modified: conflict.remote && conflict.remote.last_modified });
+ // If the resolution object is strictly equal to the
+ // remote record, then we can mark it as synced locally.
+ // Otherwise, mark it as updated (so that the resolution is pushed).
+ const synced = deepEqual(resolved, conflict.remote);
+ return markStatus(resolved, synced ? "synced" : "updated");
+ }
+ /**
+ * Synchronize remote and local data. The promise will resolve with a
+ * {@link SyncResultObject}, though will reject:
+ *
+ * - if the server is currently backed off;
+ * - if the server has been detected flushed.
+ *
+ * Options:
+ * - {Object} headers: HTTP headers to attach to outgoing requests.
+ * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
+ * - {Number} retry: Number of retries when server fails to process the request (default: 1).
+ * - {Collection.strategy} strategy: See {@link Collection.strategy}.
+ * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
+ * backed off.
+ * - {String} bucket: The remove bucket id to use (default: null)
+ * - {String} collection: The remove collection id to use (default: null)
+ * - {String} remote The remote Kinto server endpoint to use (default: null).
+ *
+ * @param {Object} options Options.
+ * @return {Promise}
+ * @throws {Error} If an invalid remote option is passed.
+ */
+ async sync(options = {
+ strategy: Collection.strategy.MANUAL,
+ headers: {},
+ retry: 1,
+ ignoreBackoff: false,
+ bucket: null,
+ collection: null,
+ remote: null,
+ expectedTimestamp: null,
+ }) {
+ options = Object.assign(Object.assign({}, options), { bucket: options.bucket || this.bucket, collection: options.collection || this.name });
+ const previousRemote = this.api.remote;
+ if (options.remote) {
+ // Note: setting the remote ensures it's valid, throws when invalid.
+ this.api.remote = options.remote;
+ }
+ if (!options.ignoreBackoff && this.api.backoff > 0) {
+ const seconds = Math.ceil(this.api.backoff / 1000);
+ return Promise.reject(new Error(`Server is asking clients to back off; retry in ${seconds}s or use the ignoreBackoff option.`));
+ }
+ const client = this.api
+ .bucket(options.bucket)
+ .collection(options.collection);
+ const result = new SyncResultObject();
+ try {
+ // Fetch collection metadata.
+ await this.pullMetadata(client, options);
+ // Fetch last changes from the server.
+ await this.pullChanges(client, result, options);
+ const { lastModified } = result;
+ if (options.strategy != Collection.strategy.PULL_ONLY) {
+ // Fetch local changes
+ const toSync = await this.gatherLocalChanges();
+ // Publish local changes and pull local resolutions
+ await this.pushChanges(client, toSync, result, options);
+ // Publish local resolution of push conflicts to server (on CLIENT_WINS)
+ const resolvedUnsynced = result.resolved.filter(r => r._status !== "synced");
+ if (resolvedUnsynced.length > 0) {
+ const resolvedEncoded = await Promise.all(resolvedUnsynced.map(resolution => {
+ let record = resolution.accepted;
+ if (record === null) {
+ record = { id: resolution.id, _status: resolution._status };
+ }
+ return this._encodeRecord("remote", record);
+ }));
+ await this.pushChanges(client, resolvedEncoded, result, options);
+ }
+ // Perform a last pull to catch changes that occured after the last pull,
+ // while local changes were pushed. Do not do it nothing was pushed.
+ if (result.published.length > 0) {
+ // Avoid redownloading our own changes during the last pull.
+ const pullOpts = Object.assign(Object.assign({}, options), { lastModified, exclude: result.published });
+ await this.pullChanges(client, result, pullOpts);
+ }
+ }
+ // Don't persist lastModified value if any conflict or error occured
+ if (result.ok) {
+ // No conflict occured, persist collection's lastModified value
+ this._lastModified = await this.db.saveLastModified(result.lastModified);
+ }
+ }
+ catch (e) {
+ this.events.emit("sync:error", Object.assign(Object.assign({}, options), { error: e }));
+ throw e;
+ }
+ finally {
+ // Ensure API default remote is reverted if a custom one's been used
+ this.api.remote = previousRemote;
+ }
+ this.events.emit("sync:success", Object.assign(Object.assign({}, options), { result }));
+ return result;
+ }
+ /**
+ * Load a list of records already synced with the remote server.
+ *
+ * The local records which are unsynced or whose timestamp is either missing
+ * or superior to those being loaded will be ignored.
+ *
+ * @deprecated Use {@link importBulk} instead.
+ * @param {Array} records The previously exported list of records to load.
+ * @return {Promise} with the effectively imported records.
+ */
+ async loadDump(records) {
+ return this.importBulk(records);
+ }
+ /**
+ * Load a list of records already synced with the remote server.
+ *
+ * The local records which are unsynced or whose timestamp is either missing
+ * or superior to those being loaded will be ignored.
+ *
+ * @param {Array} records The previously exported list of records to load.
+ * @return {Promise} with the effectively imported records.
+ */
+ async importBulk(records) {
+ if (!Array.isArray(records)) {
+ throw new Error("Records is not an array.");
+ }
+ for (const record of records) {
+ if (!Object.prototype.hasOwnProperty.call(record, "id") ||
+ !this.idSchema.validate(record.id)) {
+ throw new Error("Record has invalid ID: " + JSON.stringify(record));
+ }
+ if (!record.last_modified) {
+ throw new Error("Record has no last_modified value: " + JSON.stringify(record));
+ }
+ }
+ // Fetch all existing records from local database,
+ // and skip those who are newer or not marked as synced.
+ // XXX filter by status / ids in records
+ const { data } = await this.list({}, { includeDeleted: true });
+ const existingById = data.reduce((acc, record) => {
+ acc[record.id] = record;
+ return acc;
+ }, {});
+ const newRecords = records.filter(record => {
+ const localRecord = existingById[record.id];
+ const shouldKeep =
+ // No local record with this id.
+ localRecord === undefined ||
+ // Or local record is synced
+ (localRecord._status === "synced" &&
+ // And was synced from server
+ localRecord.last_modified !== undefined &&
+ // And is older than imported one.
+ record.last_modified > localRecord.last_modified);
+ return shouldKeep;
+ });
+ return await this.db.importBulk(newRecords.map(markSynced));
+ }
+ async pullMetadata(client, options = {}) {
+ const { expectedTimestamp, headers } = options;
+ const query = expectedTimestamp
+ ? { query: { _expected: expectedTimestamp } }
+ : undefined;
+ const metadata = await client.getData(Object.assign(Object.assign({}, query), { headers }));
+ return this.db.saveMetadata(metadata);
+ }
+ async metadata() {
+ return this.db.getMetadata();
+ }
+}
+/**
+ * A Collection-oriented wrapper for an adapter's transaction.
+ *
+ * This defines the high-level functions available on a collection.
+ * The collection itself offers functions of the same name. These will
+ * perform just one operation in its own transaction.
+ */
+class CollectionTransaction {
+ constructor(collection, adapterTransaction) {
+ this.collection = collection;
+ this.adapterTransaction = adapterTransaction;
+ this._events = [];
+ }
+ _queueEvent(action, payload) {
+ this._events.push({ action, payload });
+ }
+ /**
+ * Emit queued events, to be called once every transaction operations have
+ * been executed successfully.
+ */
+ emitEvents() {
+ for (const { action, payload } of this._events) {
+ this.collection.events.emit(action, payload);
+ }
+ if (this._events.length > 0) {
+ const targets = this._events.map(({ action, payload }) => (Object.assign({ action }, payload)));
+ this.collection.events.emit("change", { targets });
+ }
+ this._events = [];
+ }
+ /**
+ * Retrieve a record by its id from the local database, or
+ * undefined if none exists.
+ *
+ * This will also return virtually deleted records.
+ *
+ * @param {String} id
+ * @return {Object}
+ */
+ getAny(id) {
+ const record = this.adapterTransaction.get(id);
+ return { data: record, permissions: {} };
+ }
+ /**
+ * Retrieve a record by its id from the local database.
+ *
+ * Options:
+ * - {Boolean} includeDeleted: Include virtually deleted records.
+ *
+ * @param {String} id
+ * @param {Object} options
+ * @return {Object}
+ */
+ get(id, options = { includeDeleted: false }) {
+ const res = this.getAny(id);
+ if (!res.data ||
+ (!options.includeDeleted && res.data._status === "deleted")) {
+ throw new Error(`Record with id=${id} not found.`);
+ }
+ return res;
+ }
+ /**
+ * Deletes a record from the local database.
+ *
+ * Options:
+ * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
+ * update its `_status` attribute to `deleted` instead (default: true)
+ *
+ * @param {String} id The record's Id.
+ * @param {Object} options The options object.
+ * @return {Object}
+ */
+ delete(id, options = { virtual: true }) {
+ // Ensure the record actually exists.
+ const existing = this.adapterTransaction.get(id);
+ const alreadyDeleted = existing && existing._status == "deleted";
+ if (!existing || (alreadyDeleted && options.virtual)) {
+ throw new Error(`Record with id=${id} not found.`);
+ }
+ // Virtual updates status.
+ if (options.virtual) {
+ this.adapterTransaction.update(markDeleted(existing));
+ }
+ else {
+ // Delete for real.
+ this.adapterTransaction.delete(id);
+ }
+ this._queueEvent("delete", { data: existing });
+ return { data: existing, permissions: {} };
+ }
+ /**
+ * Soft delete all records from the local database.
+ *
+ * @param {Array} ids Array of non-deleted Record Ids.
+ * @return {Object}
+ */
+ deleteAll(ids) {
+ const existingRecords = [];
+ ids.forEach(id => {
+ existingRecords.push(this.adapterTransaction.get(id));
+ this.delete(id);
+ });
+ this._queueEvent("deleteAll", { data: existingRecords });
+ return { data: existingRecords, permissions: {} };
+ }
+ /**
+ * Deletes a record from the local database, if any exists.
+ * Otherwise, do nothing.
+ *
+ * @param {String} id The record's Id.
+ * @return {Object}
+ */
+ deleteAny(id) {
+ const existing = this.adapterTransaction.get(id);
+ if (existing) {
+ this.adapterTransaction.update(markDeleted(existing));
+ this._queueEvent("delete", { data: existing });
+ }
+ return { data: Object.assign({ id }, existing), deleted: !!existing, permissions: {} };
+ }
+ /**
+ * Adds a record to the local database, asserting that none
+ * already exist with this ID.
+ *
+ * @param {Object} record, which must contain an ID
+ * @return {Object}
+ */
+ create(record) {
+ if (typeof record !== "object") {
+ throw new Error("Record is not an object.");
+ }
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) {
+ throw new Error("Cannot create a record missing id");
+ }
+ if (!this.collection.idSchema.validate(record.id)) {
+ throw new Error(`Invalid Id: ${record.id}`);
+ }
+ this.adapterTransaction.create(record);
+ this._queueEvent("create", { data: record });
+ return { data: record, permissions: {} };
+ }
+ /**
+ * Updates a record from the local database.
+ *
+ * Options:
+ * - {Boolean} synced: Sets record status to "synced" (default: false)
+ * - {Boolean} patch: Extends the existing record instead of overwriting it
+ * (default: false)
+ *
+ * @param {Object} record
+ * @param {Object} options
+ * @return {Object}
+ */
+ update(record, options = { synced: false, patch: false }) {
+ if (typeof record !== "object") {
+ throw new Error("Record is not an object.");
+ }
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) {
+ throw new Error("Cannot update a record missing id.");
+ }
+ if (!this.collection.idSchema.validate(record.id)) {
+ throw new Error(`Invalid Id: ${record.id}`);
+ }
+ const oldRecord = this.adapterTransaction.get(record.id);
+ if (!oldRecord) {
+ throw new Error(`Record with id=${record.id} not found.`);
+ }
+ const newRecord = options.patch ? Object.assign(Object.assign({}, oldRecord), record) : record;
+ const updated = this._updateRaw(oldRecord, newRecord, options);
+ this.adapterTransaction.update(updated);
+ this._queueEvent("update", { data: updated, oldRecord });
+ return { data: updated, oldRecord, permissions: {} };
+ }
+ /**
+ * Lower-level primitive for updating a record while respecting
+ * _status and last_modified.
+ *
+ * @param {Object} oldRecord: the record retrieved from the DB
+ * @param {Object} newRecord: the record to replace it with
+ * @return {Object}
+ */
+ _updateRaw(oldRecord, newRecord, { synced = false } = {}) {
+ const updated = Object.assign({}, newRecord);
+ // Make sure to never loose the existing timestamp.
+ if (oldRecord && oldRecord.last_modified && !updated.last_modified) {
+ updated.last_modified = oldRecord.last_modified;
+ }
+ // If only local fields have changed, then keep record as synced.
+ // If status is created, keep record as created.
+ // If status is deleted, mark as updated.
+ const isIdentical = oldRecord &&
+ recordsEqual(oldRecord, updated, this.collection.localFields);
+ const keepSynced = isIdentical && oldRecord._status == "synced";
+ const neverSynced = !oldRecord || (oldRecord && oldRecord._status == "created");
+ const newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated";
+ return markStatus(updated, newStatus);
+ }
+ /**
+ * Upsert a record into the local database.
+ *
+ * This record must have an ID.
+ *
+ * If a record with this ID already exists, it will be replaced.
+ * Otherwise, this record will be inserted.
+ *
+ * @param {Object} record
+ * @return {Object}
+ */
+ upsert(record) {
+ if (typeof record !== "object") {
+ throw new Error("Record is not an object.");
+ }
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) {
+ throw new Error("Cannot update a record missing id.");
+ }
+ if (!this.collection.idSchema.validate(record.id)) {
+ throw new Error(`Invalid Id: ${record.id}`);
+ }
+ let oldRecord = this.adapterTransaction.get(record.id);
+ const updated = this._updateRaw(oldRecord, record);
+ this.adapterTransaction.update(updated);
+ // Don't return deleted records -- pretend they are gone
+ if (oldRecord && oldRecord._status == "deleted") {
+ oldRecord = undefined;
+ }
+ if (oldRecord) {
+ this._queueEvent("update", { data: updated, oldRecord });
+ }
+ else {
+ this._queueEvent("create", { data: updated });
+ }
+ return { data: updated, oldRecord, permissions: {} };
+ }
+}
+
+const DEFAULT_BUCKET_NAME = "default";
+const DEFAULT_REMOTE = "http://localhost:8888/v1";
+const DEFAULT_RETRY = 1;
+/**
+ * KintoBase class.
+ */
+class KintoBase {
+ /**
+ * Provides a public access to the base adapter class. Users can create a
+ * custom DB adapter by extending {@link BaseAdapter}.
+ *
+ * @type {Object}
+ */
+ static get adapters() {
+ return {
+ BaseAdapter: BaseAdapter,
+ };
+ }
+ /**
+ * Synchronization strategies. Available strategies are:
+ *
+ * - `MANUAL`: Conflicts will be reported in a dedicated array.
+ * - `SERVER_WINS`: Conflicts are resolved using remote data.
+ * - `CLIENT_WINS`: Conflicts are resolved using local data.
+ *
+ * @type {Object}
+ */
+ static get syncStrategy() {
+ return Collection.strategy;
+ }
+ /**
+ * Constructor.
+ *
+ * Options:
+ * - `{String}` `remote` The server URL to use.
+ * - `{String}` `bucket` The collection bucket name.
+ * - `{EventEmitter}` `events` Events handler.
+ * - `{BaseAdapter}` `adapter` The base DB adapter class.
+ * - `{Object}` `adapterOptions` Options given to the adapter.
+ * - `{Object}` `headers` The HTTP headers to use.
+ * - `{Object}` `retry` Number of retries when the server fails to process the request (default: `1`)
+ * - `{String}` `requestMode` The HTTP CORS mode to use.
+ * - `{Number}` `timeout` The requests timeout in ms (default: `5000`).
+ *
+ * @param {Object} options The options object.
+ */
+ constructor(options = {}) {
+ const defaults = {
+ bucket: DEFAULT_BUCKET_NAME,
+ remote: DEFAULT_REMOTE,
+ retry: DEFAULT_RETRY,
+ };
+ this._options = Object.assign(Object.assign({}, defaults), options);
+ if (!this._options.adapter) {
+ throw new Error("No adapter provided");
+ }
+ this._api = null;
+ /**
+ * The event emitter instance.
+ * @type {EventEmitter}
+ */
+ this.events = this._options.events;
+ }
+ /**
+ * The kinto HTTP client instance.
+ * @type {KintoClient}
+ */
+ get api() {
+ const { events, headers, remote, requestMode, retry, timeout, } = this._options;
+ if (!this._api) {
+ this._api = new this.ApiClass(remote, {
+ events,
+ headers,
+ requestMode,
+ retry,
+ timeout,
+ });
+ }
+ return this._api;
+ }
+ /**
+ * Creates a {@link Collection} instance. The second (optional) parameter
+ * will set collection-level options like e.g. `remoteTransformers`.
+ *
+ * @param {String} collName The collection name.
+ * @param {Object} [options={}] Extra options or override client's options.
+ * @param {Object} [options.idSchema] IdSchema instance (default: UUID)
+ * @param {Object} [options.remoteTransformers] Array<RemoteTransformer> (default: `[]`])
+ * @param {Object} [options.hooks] Array<Hook> (default: `[]`])
+ * @param {Object} [options.localFields] Array<Field> (default: `[]`])
+ * @return {Collection}
+ */
+ collection(collName, options = {}) {
+ if (!collName) {
+ throw new Error("missing collection name");
+ }
+ const { bucket, events, adapter, adapterOptions } = Object.assign(Object.assign({}, this._options), options);
+ const { idSchema, remoteTransformers, hooks, localFields } = options;
+ return new Collection(bucket, collName, this, {
+ events,
+ adapter,
+ adapterOptions,
+ idSchema,
+ remoteTransformers,
+ hooks,
+ localFields,
+ });
+ }
+}
+
+/*
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
+ // Use standalone kinto-http module landed in FFx.
+ KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs"
+});
+export class Kinto extends KintoBase {
+ static get adapters() {
+ return {
+ BaseAdapter,
+ IDB,
+ };
+ }
+ get ApiClass() {
+ return lazy.KintoHttpClient;
+ }
+ constructor(options = {}) {
+ const events = {};
+ lazy.EventEmitter.decorate(events);
+ const defaults = {
+ adapter: IDB,
+ events,
+ };
+ super(Object.assign(Object.assign({}, defaults), options));
+ }
+ collection(collName, options = {}) {
+ const idSchema = {
+ validate(id) {
+ return typeof id == "string" && RE_RECORD_ID.test(id);
+ },
+ generate() {
+ return Services.uuid.generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ },
+ };
+ return super.collection(collName, Object.assign({ idSchema }, options));
+ }
+}
+
diff --git a/services/common/kinto-storage-adapter.sys.mjs b/services/common/kinto-storage-adapter.sys.mjs
index 75c6c06a26..e104a6cbd1 100644
--- a/services/common/kinto-storage-adapter.sys.mjs
+++ b/services/common/kinto-storage-adapter.sys.mjs
@@ -13,9 +13,7 @@
*/
import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs";
-const { Kinto } = ChromeUtils.import(
- "resource://services-common/kinto-offline-client.js"
-);
+import { Kinto } from "resource://services-common/kinto-offline-client.sys.mjs";
/**
* Filter and sort list against provided filters and order.
diff --git a/services/common/modules-testing/logging.sys.mjs b/services/common/modules-testing/logging.sys.mjs
index 63bd306b67..c2a0ef5198 100644
--- a/services/common/modules-testing/logging.sys.mjs
+++ b/services/common/modules-testing/logging.sys.mjs
@@ -51,6 +51,6 @@ export function initTestLogging(level) {
return logStats;
}
-export function getTestLogger(component) {
+export function getTestLogger() {
return Log.repository.getLogger("Testing");
}
diff --git a/services/common/moz.build b/services/common/moz.build
index 3585dc5426..144ccaff04 100644
--- a/services/common/moz.build
+++ b/services/common/moz.build
@@ -20,7 +20,7 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES["services-common"] += [
"async.sys.mjs",
"kinto-http-client.sys.mjs",
- "kinto-offline-client.js",
+ "kinto-offline-client.sys.mjs",
"kinto-storage-adapter.sys.mjs",
"logmanager.sys.mjs",
"observers.sys.mjs",
diff --git a/services/common/tests/unit/head_helpers.js b/services/common/tests/unit/head_helpers.js
index bd994ee71a..6b831eef01 100644
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -91,7 +91,7 @@ function do_check_throws_message(aFunc, aResult) {
* @usage _("Hello World") -> prints "Hello World"
* @usage _(1, 2, 3) -> prints "1 2 3"
*/
-var _ = function (some, debug, text, to) {
+var _ = function () {
print(Array.from(arguments).join(" "));
};
@@ -192,7 +192,7 @@ var PACSystemSettings = {
// each test gets a completely fresh setup.
mainThreadOnly: true,
PACURI: null,
- getProxyForURI: function getProxyForURI(aURI) {
+ getProxyForURI: function getProxyForURI() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
},
};
@@ -221,7 +221,7 @@ function getUptakeTelemetrySnapshot(component, source) {
return (
parentEvents
// Transform raw event data to objects.
- .map(([i, category, method, object, value, extras]) => {
+ .map(([, category, method, object, value, extras]) => {
return { category, method, object, value, extras };
})
// Keep only for the specified component and source.
diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js
index 4b5e8471b1..154976d701 100644
--- a/services/common/tests/unit/test_kinto.js
+++ b/services/common/tests/unit/test_kinto.js
@@ -1,8 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-const { Kinto } = ChromeUtils.import(
- "resource://services-common/kinto-offline-client.js"
+const { Kinto } = ChromeUtils.importESModule(
+ "resource://services-common/kinto-offline-client.sys.mjs"
);
const { FirefoxAdapter } = ChromeUtils.importESModule(
"resource://services-common/kinto-storage-adapter.sys.mjs"
diff --git a/services/common/tests/unit/test_utils_namedTimer.js b/services/common/tests/unit/test_utils_namedTimer.js
index 47ac0b9dc1..d7e6768da8 100644
--- a/services/common/tests/unit/test_utils_namedTimer.js
+++ b/services/common/tests/unit/test_utils_namedTimer.js
@@ -40,7 +40,7 @@ add_test(function test_delay() {
const delay = 100;
let that = {};
let t0 = Date.now();
- function callback(timer) {
+ function callback() {
// Difference should be ~2*delay, but hard to predict on all platforms,
// particularly Windows XP.
Assert.ok(Date.now() - t0 > delay);
@@ -57,7 +57,7 @@ add_test(function test_clear() {
const delay = 0;
let that = {};
CommonUtils.namedTimer(
- function callback(timer) {
+ function callback() {
do_throw("Shouldn't fire!");
},
delay,
diff --git a/services/crypto/modules/WeaveCrypto.sys.mjs b/services/crypto/modules/WeaveCrypto.sys.mjs
index 63ee51a1a2..4a9aafd4c0 100644
--- a/services/crypto/modules/WeaveCrypto.sys.mjs
+++ b/services/crypto/modules/WeaveCrypto.sys.mjs
@@ -25,7 +25,7 @@ WeaveCrypto.prototype = {
"nsISupportsWeakReference",
]),
- observe(subject, topic, data) {
+ observe(subject, topic) {
let self = this._self;
self.log("Observed " + topic + " topic.");
if (topic == "nsPref:changed") {
diff --git a/services/crypto/tests/unit/head_helpers.js b/services/crypto/tests/unit/head_helpers.js
index 322f6761a9..e945ac1182 100644
--- a/services/crypto/tests/unit/head_helpers.js
+++ b/services/crypto/tests/unit/head_helpers.js
@@ -73,6 +73,6 @@ addResourceAlias();
* @usage _("Hello World") -> prints "Hello World"
* @usage _(1, 2, 3) -> prints "1 2 3"
*/
-var _ = function (some, debug, text, to) {
+var _ = function () {
print(Array.from(arguments).join(" "));
};
diff --git a/services/fxaccounts/FxAccounts.sys.mjs b/services/fxaccounts/FxAccounts.sys.mjs
index 18169c6b2d..790a6195f8 100644
--- a/services/fxaccounts/FxAccounts.sys.mjs
+++ b/services/fxaccounts/FxAccounts.sys.mjs
@@ -65,6 +65,8 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
+export const ERROR_INVALID_ACCOUNT_STATE = "ERROR_INVALID_ACCOUNT_STATE";
+
// An AccountState object holds all state related to one specific account.
// It is considered "private" to the FxAccounts modules.
// Only one AccountState is ever "current" in the FxAccountsInternal object -
@@ -170,7 +172,7 @@ AccountState.prototype = {
delete updatedFields.uid;
}
if (!this.isCurrent) {
- return Promise.reject(new Error("Another user has signed in"));
+ return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
}
return this.storageManager.updateAccountData(updatedFields);
},
@@ -179,11 +181,11 @@ AccountState.prototype = {
if (!this.isCurrent) {
log.info(
"An accountState promise was resolved, but was actually rejected" +
- " due to a different user being signed in. Originally resolved" +
- " with",
+ " due to the account state changing. This can happen if a new account signed in, or" +
+ " the account was signed out. Originally resolved with, ",
result
);
- return Promise.reject(new Error("A different user signed in"));
+ return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
}
return Promise.resolve(result);
},
@@ -195,12 +197,13 @@ AccountState.prototype = {
// problems.
if (!this.isCurrent) {
log.info(
- "An accountState promise was rejected, but we are ignoring that " +
- "reason and rejecting it due to a different user being signed in. " +
- "Originally rejected with",
+ "An accountState promise was rejected, but we are ignoring that" +
+ " reason and rejecting it due to the account state changing. This can happen if" +
+ " a different account signed in or the account was signed out" +
+ " originally resolved with, ",
error
);
- return Promise.reject(new Error("A different user signed in"));
+ return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
}
return Promise.reject(error);
},
@@ -215,7 +218,7 @@ AccountState.prototype = {
// A preamble for the cache helpers...
_cachePreamble() {
if (!this.isCurrent) {
- throw new Error("Another user has signed in");
+ throw new Error(ERROR_INVALID_ACCOUNT_STATE);
}
},
@@ -466,6 +469,37 @@ export class FxAccounts {
}
}
+ /** Gets both the OAuth token and the users scoped keys for that token
+ * and verifies that both operations were done for the same user,
+ * preventing race conditions where a caller
+ * can get the key for one user, and the id of another if the user
+ * is rapidly switching between accounts
+ *
+ * @param options
+ * {
+ * scope: string the oauth scope being requested. This must
+ * be a scope with an associated key, otherwise an error
+ * will be thrown that the key is not available.
+ * ttl: (number) OAuth token TTL in seconds
+ * }
+ *
+ * @return Promise.<Object | Error>
+ * The promise resolve to both the access token being requested, and the scoped key
+ * {
+ * token: (string) access token
+ * key: (object) the scoped key object
+ * }
+ * The promise can reject, with one of the errors `getOAuthToken`, `FxAccountKeys.getKeyForScope`, or
+ * error if the user changed in-between operations
+ */
+ getOAuthTokenAndKey(options = {}) {
+ return this._withCurrentAccountState(async () => {
+ const key = await this.keys.getKeyForScope(options.scope);
+ const token = await this.getOAuthToken(options);
+ return { token, key };
+ });
+ }
+
/**
* Remove an OAuth token from the token cache. Callers should call this
* after they determine a token is invalid, so a new token will be fetched
diff --git a/services/fxaccounts/FxAccountsClient.sys.mjs b/services/fxaccounts/FxAccountsClient.sys.mjs
index 9dc80ff419..a62da40fd8 100644
--- a/services/fxaccounts/FxAccountsClient.sys.mjs
+++ b/services/fxaccounts/FxAccountsClient.sys.mjs
@@ -59,7 +59,7 @@ FxAccountsClient.prototype = {
/*
* Return current time in milliseconds
*
- * Not used by this module, but made available to the FxAccounts.jsm
+ * Not used by this module, but made available to the FxAccounts.sys.mjs
* that uses this client.
*/
now() {
@@ -498,7 +498,7 @@ FxAccountsClient.prototype = {
*/
accountExists(email) {
return this.signIn(email, "").then(
- cantHappen => {
+ () => {
throw new Error("How did I sign in with an empty password?");
},
expectedError => {
diff --git a/services/fxaccounts/FxAccountsProfile.sys.mjs b/services/fxaccounts/FxAccountsProfile.sys.mjs
index de8bdb2f0e..8022a6d8a8 100644
--- a/services/fxaccounts/FxAccountsProfile.sys.mjs
+++ b/services/fxaccounts/FxAccountsProfile.sys.mjs
@@ -52,7 +52,7 @@ FxAccountsProfile.prototype = {
// making another request to determine if it is fresh or not.
PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
- observe(subject, topic, data) {
+ observe(subject, topic) {
// If we get a profile change notification from our webchannel it means
// the user has just changed their profile via the web, so we want to
// ignore our "freshness threshold"
diff --git a/services/fxaccounts/FxAccountsWebChannel.sys.mjs b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
index fdd0b75e93..f8d7a3362d 100644
--- a/services/fxaccounts/FxAccountsWebChannel.sys.mjs
+++ b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
@@ -431,7 +431,7 @@ FxAccountsWebChannelHelpers.prototype = {
// A sync-specific hack - we want to ensure sync has been initialized
// before we set the signed-in user.
// XXX - probably not true any more, especially now we have observerPreloads
- // in FxAccounts.jsm?
+ // in FxAccounts.sys.mjs?
let xps =
this._weaveXPCOM ||
Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
diff --git a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
index 4b5e943591..24353afbc1 100644
--- a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
+++ b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
@@ -49,7 +49,7 @@ MockStorage.prototype = Object.freeze({
getOAuthTokens() {
return Promise.resolve(null);
},
- setOAuthTokens(contents) {
+ setOAuthTokens() {
return Promise.resolve();
},
});
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js
index c4aec73a03..239adb206f 100644
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -6,7 +6,7 @@
const { CryptoUtils } = ChromeUtils.importESModule(
"resource://services-crypto/utils.sys.mjs"
);
-const { FxAccounts } = ChromeUtils.importESModule(
+const { FxAccounts, ERROR_INVALID_ACCOUNT_STATE } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
);
const { FxAccountsClient } = ChromeUtils.importESModule(
@@ -120,7 +120,7 @@ function MockFxAccountsClient() {
// mock calls up to the auth server to determine whether the
// user account has been verified
- this.recoveryEmailStatus = async function (sessionToken) {
+ this.recoveryEmailStatus = async function () {
// simulate a call to /recovery_email/status
return {
email: this._email,
@@ -139,7 +139,7 @@ function MockFxAccountsClient() {
return !this._deletedOnServer;
};
- this.accountKeys = function (keyFetchToken) {
+ this.accountKeys = function () {
return new Promise(resolve => {
do_timeout(50, () => {
resolve({
@@ -188,7 +188,7 @@ Object.setPrototypeOf(
* mock the now() method, so that we can simulate the passing of
* time and verify that signatures expire correctly.
*/
-function MockFxAccounts(credentials = null) {
+function MockFxAccounts() {
let result = new FxAccounts({
VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
@@ -453,10 +453,10 @@ add_test(function test_polling_timeout() {
fxa.setSignedInUser(test_user).then(() => {
p.then(
- success => {
+ () => {
do_throw("this should not succeed");
},
- fail => {
+ () => {
removeObserver();
fxa.signOut().then(run_next_test);
}
@@ -471,7 +471,7 @@ add_task(async function test_onverified_once() {
let numNotifications = 0;
- function observe(aSubject, aTopic, aData) {
+ function observe() {
numNotifications += 1;
}
Services.obs.addObserver(observe, ONVERIFIED_NOTIFICATION);
@@ -777,11 +777,10 @@ add_task(async function test_getKeyForScope_nonexistent_account() {
});
});
- // XXX - the exception message here isn't ideal, but doesn't really matter...
- await Assert.rejects(
- fxa.keys.getKeyForScope(SCOPE_OLD_SYNC),
- /A different user signed in/
- );
+ await Assert.rejects(fxa.keys.getKeyForScope(SCOPE_OLD_SYNC), err => {
+ Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
+ return true; // expected error
+ });
await promiseLogout;
@@ -972,17 +971,17 @@ add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() {
makeObserver(ONLOGOUT_NOTIFICATION, function () {
log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
- fxa._internal.getUserAccountData().then(user2 => {
+ fxa._internal.getUserAccountData().then(() => {
fxa._internal.abortExistingFlow().then(run_next_test);
});
});
fxa
.setSignedInUser(user)
- .then(user2 => {
+ .then(() => {
return fxa.keys._fetchAndUnwrapAndDeriveKeys();
})
- .catch(error => {
+ .catch(() => {
log.info("setSignedInUser correctly rejected");
});
});
@@ -1273,11 +1272,7 @@ add_task(async function test_getOAuthTokenCachedScopeNormalization() {
let numOAuthTokenCalls = 0;
let client = fxa._internal.fxAccountsClient;
- client.accessTokenWithSessionToken = async (
- _sessionTokenHex,
- _clientId,
- scopeString
- ) => {
+ client.accessTokenWithSessionToken = async (_sessionTokenHex, _clientId) => {
numOAuthTokenCalls++;
return MOCK_TOKEN_RESPONSE;
};
@@ -1405,6 +1400,31 @@ add_test(function test_getOAuthToken_error() {
});
});
+add_test(async function test_getOAuthTokenAndKey_errors_if_user_change() {
+ const fxa = new MockFxAccounts();
+ const alice = getTestUser("alice");
+ const bob = getTestUser("bob");
+ alice.verified = true;
+ bob.verified = true;
+
+ fxa.getOAuthToken = async () => {
+ // We mock what would happen if the user got changed
+ // after we got the access token
+ await fxa.setSignedInUser(bob);
+ return "access token";
+ };
+ fxa.keys.getKeyForScope = () => Promise.resolve("key!");
+ await fxa.setSignedInUser(alice);
+ await Assert.rejects(
+ fxa.getOAuthTokenAndKey({ scope: "foo", ttl: 10 }),
+ err => {
+ Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
+ return true; // expected error
+ }
+ );
+ run_next_test();
+});
+
add_task(async function test_listAttachedOAuthClients() {
const ONE_HOUR = 60 * 60 * 1000;
const ONE_DAY = 24 * ONE_HOUR;
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
index 68337eb69e..4b6ac58879 100644
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -80,7 +80,7 @@ function MockFxAccountsClient(device) {
// mock calls up to the auth server to determine whether the
// user account has been verified
- this.recoveryEmailStatus = function (sessionToken) {
+ this.recoveryEmailStatus = function () {
// simulate a call to /recovery_email/status
return Promise.resolve({
email: this._email,
@@ -104,8 +104,7 @@ function MockFxAccountsClient(device) {
return Promise.resolve(!!uid && !this._deletedOnServer);
};
- this.registerDevice = (st, name, type) =>
- Promise.resolve({ id: device.id, name });
+ this.registerDevice = (st, name) => Promise.resolve({ id: device.id, name });
this.updateDevice = (st, id, name) => Promise.resolve({ id, name });
this.signOut = () => Promise.resolve({});
this.getDeviceList = st =>
@@ -655,7 +654,7 @@ add_task(async function test_verification_updates_registration() {
};
});
- fxa._internal.checkEmailStatus = async function (sessionToken) {
+ fxa._internal.checkEmailStatus = async function () {
credentials.verified = true;
return credentials;
};
@@ -792,7 +791,7 @@ add_task(async function test_refreshDeviceList() {
};
const deviceListUpdateObserver = {
count: 0,
- observe(subject, topic, data) {
+ observe() {
this.count++;
},
};
diff --git a/services/fxaccounts/tests/xpcshell/test_commands.js b/services/fxaccounts/tests/xpcshell/test_commands.js
index 3fa42da439..c72f76193a 100644
--- a/services/fxaccounts/tests/xpcshell/test_commands.js
+++ b/services/fxaccounts/tests/xpcshell/test_commands.js
@@ -174,7 +174,7 @@ add_task(async function test_sendtab_receive() {
const fxai = FxaInternalMock();
const sendTab = new SendTab(commands, fxai);
- sendTab._encrypt = (bytes, device) => {
+ sendTab._encrypt = bytes => {
return bytes;
};
sendTab._decrypt = bytes => {
@@ -387,7 +387,7 @@ add_task(async function test_commands_handleCommands() {
},
};
const commands = new FxAccountsCommands(fxAccounts);
- commands.sendTab.handle = (sender, data, reason) => {
+ commands.sendTab.handle = () => {
return {
title: "testTitle",
uri: "https://testURI",
@@ -436,7 +436,7 @@ add_task(async function test_commands_handleCommands_invalid_tab() {
},
};
const commands = new FxAccountsCommands(fxAccounts);
- commands.sendTab.handle = (sender, data, reason) => {
+ commands.sendTab.handle = () => {
return {
title: "badUriTab",
uri: "file://path/to/pdf",
diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
index 798c439212..8575f7065c 100644
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -94,7 +94,7 @@ Object.setPrototypeOf(
FxAccountsClient.prototype
);
-function MockFxAccounts(device = {}) {
+function MockFxAccounts() {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
newAccountState(credentials) {
diff --git a/services/fxaccounts/tests/xpcshell/test_pairing.js b/services/fxaccounts/tests/xpcshell/test_pairing.js
index eac3112242..245db66f66 100644
--- a/services/fxaccounts/tests/xpcshell/test_pairing.js
+++ b/services/fxaccounts/tests/xpcshell/test_pairing.js
@@ -61,7 +61,7 @@ const fxAccounts = {
},
_internal: {
keys: {
- getKeyForScope(scope) {
+ getKeyForScope() {
return {
kid: "123456",
k: KSYNC,
diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js
index f8137b5691..39acf653d4 100644
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -142,10 +142,10 @@ add_test(function fetchAndCacheProfile_always_bumps_cachedAt() {
profile._cachedAt = 12345;
return profile._fetchAndCacheProfile().then(
- result => {
+ () => {
do_throw("Should not succeed");
},
- err => {
+ () => {
Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped");
run_next_test();
}
@@ -164,7 +164,7 @@ add_test(function fetchAndCacheProfile_sendsETag() {
};
let profile = CreateFxAccountsProfile(fxa, client);
- return profile._fetchAndCacheProfile().then(result => {
+ return profile._fetchAndCacheProfile().then(() => {
run_next_test();
});
});
@@ -282,7 +282,7 @@ add_test(function fetchAndCacheProfile_alreadyCached() {
};
let profile = CreateFxAccountsProfile(fxa, client);
- profile._cacheProfile = function (toCache) {
+ profile._cacheProfile = function () {
do_throw("This method should not be called.");
};
@@ -614,7 +614,7 @@ add_test(function getProfile_has_cached_fetch_deleted() {
// instead of checking this in a mocked "save" function, just check after the
// observer
- makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+ makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function () {
profile.getProfile().then(profileData => {
Assert.equal(null, profileData.avatar);
run_next_test();
diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js
index 22fcc293f8..5b6e4ffdff 100644
--- a/services/fxaccounts/tests/xpcshell/test_profile_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js
@@ -39,7 +39,7 @@ let mockResponse = function (response) {
Request.ifNoneMatchSet = true;
}
},
- async dispatch(method, payload) {
+ async dispatch() {
this.response = response;
return this.response;
},
@@ -74,7 +74,7 @@ let mockResponseError = function (error) {
return function () {
return {
setHeader() {},
- async dispatch(method, payload) {
+ async dispatch() {
throw error;
},
};
@@ -221,7 +221,7 @@ add_test(function server401ResponseThenSuccess() {
let numRequests = 0;
let numAuthHeaders = 0;
// Like mockResponse but we want access to headers etc.
- client._Request = function (requestUri) {
+ client._Request = function () {
return {
setHeader(name, value) {
if (name == "Authorization") {
@@ -229,7 +229,7 @@ add_test(function server401ResponseThenSuccess() {
Assert.equal(value, "Bearer " + lastToken);
}
},
- async dispatch(method, payload) {
+ async dispatch() {
this.response = responses[numRequests];
++numRequests;
return this.response;
@@ -283,7 +283,7 @@ add_test(function server401ResponsePersists() {
let numRequests = 0;
let numAuthHeaders = 0;
- client._Request = function (requestUri) {
+ client._Request = function () {
return {
setHeader(name, value) {
if (name == "Authorization") {
@@ -291,7 +291,7 @@ add_test(function server401ResponsePersists() {
Assert.equal(value, "Bearer " + lastToken);
}
},
- async dispatch(method, payload) {
+ async dispatch() {
this.response = response;
++numRequests;
return this.response;
diff --git a/services/fxaccounts/tests/xpcshell/test_push_service.js b/services/fxaccounts/tests/xpcshell/test_push_service.js
index 0441888847..216f5d8cc8 100644
--- a/services/fxaccounts/tests/xpcshell/test_push_service.js
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -179,7 +179,7 @@ add_test(function observePushTopicDeviceConnected() {
return this;
},
};
- let obs = (subject, topic, data) => {
+ let obs = (subject, topic) => {
Services.obs.removeObserver(obs, topic);
run_next_test();
};
@@ -392,7 +392,7 @@ add_test(function observePushTopicProfileUpdated() {
return this;
},
};
- let obs = (subject, topic, data) => {
+ let obs = (subject, topic) => {
Services.obs.removeObserver(obs, topic);
run_next_test();
};
diff --git a/services/fxaccounts/tests/xpcshell/test_storage_manager.js b/services/fxaccounts/tests/xpcshell/test_storage_manager.js
index 05c565d2f4..df29ca881d 100644
--- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js
+++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js
@@ -62,7 +62,7 @@ MockedSecureStorage.prototype = {
// "TypeError: this.STORAGE_LOCKED is not a constructor"
STORAGE_LOCKED: function () {},
/* eslint-enable object-shorthand */
- async get(uid, email) {
+ async get() {
this.fetchCount++;
if (this.locked) {
throw new this.STORAGE_LOCKED();
diff --git a/services/fxaccounts/tests/xpcshell/test_web_channel.js b/services/fxaccounts/tests/xpcshell/test_web_channel.js
index 48f043d0b9..020ac4d905 100644
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -202,7 +202,7 @@ add_test(function test_error_message_remove_profile_path() {
const toTest = Object.keys(errors).length;
for (const key in errors) {
let error = errors[key];
- channel._channel.send = (message, context) => {
+ channel._channel.send = message => {
equal(
message.data.error.message,
error.expected,
@@ -403,7 +403,7 @@ add_test(function test_fxa_status_message() {
});
channel._channel = {
- send(response, sendingContext) {
+ send(response) {
Assert.equal(response.command, "fxaccounts:fxa_status");
Assert.equal(response.messageId, 123);
@@ -513,7 +513,7 @@ add_task(async function test_helpers_login_set_previous_account_name_hash() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
_internal: {
- setSignedInUser(accountData) {
+ setSignedInUser() {
return new Promise(resolve => {
// previously signed in user preference is updated.
Assert.equal(
@@ -554,7 +554,7 @@ add_task(
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
_internal: {
- setSignedInUser(accountData) {
+ setSignedInUser() {
return new Promise(resolve => {
// previously signed in user preference should not be updated.
Assert.equal(
diff --git a/services/settings/Attachments.sys.mjs b/services/settings/Attachments.sys.mjs
index 5ddc6bb046..345bf1e8e6 100644
--- a/services/settings/Attachments.sys.mjs
+++ b/services/settings/Attachments.sys.mjs
@@ -33,6 +33,14 @@ class ServerInfoError extends Error {
}
}
+class NotFoundError extends Error {
+ constructor(url, resp) {
+ super(`Could not find ${url} in cache or dump`);
+ this.name = "NotFoundError";
+ this.resp = resp;
+ }
+}
+
// Helper for the `download` method for commonly used methods, to help with
// lazily accessing the record and attachment content.
class LazyRecordAndBuffer {
@@ -99,6 +107,9 @@ export class Downloader {
static get ServerInfoError() {
return ServerInfoError;
}
+ static get NotFoundError() {
+ return NotFoundError;
+ }
constructor(...folders) {
this.folders = ["settings", ...folders];
@@ -122,15 +133,15 @@ export class Downloader {
* @param {Object} record A Remote Settings entry with attachment.
* If omitted, the attachmentId option must be set.
* @param {Object} options Some download options.
- * @param {Number} options.retries Number of times download should be retried (default: `3`)
- * @param {Boolean} options.checkHash Check content integrity (default: `true`)
- * @param {string} options.attachmentId The attachment identifier to use for
+ * @param {Number} [options.retries] Number of times download should be retried (default: `3`)
+ * @param {Boolean} [options.checkHash] Check content integrity (default: `true`)
+ * @param {string} [options.attachmentId] The attachment identifier to use for
* caching and accessing the attachment.
* (default: `record.id`)
- * @param {Boolean} options.fallbackToCache Return the cached attachment when the
+ * @param {Boolean} [options.fallbackToCache] Return the cached attachment when the
* input record cannot be fetched.
* (default: `false`)
- * @param {Boolean} options.fallbackToDump Use the remote settings dump as a
+ * @param {Boolean} [options.fallbackToDump] Use the remote settings dump as a
* potential source of the attachment.
* (default: `false`)
* @throws {Downloader.DownloadError} if the file could not be fetched.
@@ -143,12 +154,55 @@ export class Downloader {
* `_source` `String`: identifies the source of the result. Used for testing.
*/
async download(record, options) {
+ return this.#fetchAttachment(record, options);
+ }
+
+ /**
+ * Gets an attachment from the cache or local dump, avoiding requesting it
+ * from the server.
+ * If the only found attachment hash does not match the requested record, the
+ * returned attachment may have a different record, e.g. packaged in binary
+ * resources or one that is outdated.
+ *
+ * @param {Object} record A Remote Settings entry with attachment.
+ * If omitted, the attachmentId option must be set.
+ * @param {Object} options Some download options.
+ * @param {Number} [options.retries] Number of times download should be retried (default: `3`)
+ * @param {Boolean} [options.checkHash] Check content integrity (default: `true`)
+ * @param {string} [options.attachmentId] The attachment identifier to use for
+ * caching and accessing the attachment.
+ * (default: `record.id`)
+ * @throws {Downloader.DownloadError} if the file could not be fetched.
+ * @throws {Downloader.BadContentError} if the downloaded content integrity is not valid.
+ * @throws {Downloader.ServerInfoError} if the server response is not valid.
+ * @throws {NetworkError} if fetching the server infos and fetching the attachment fails.
+ * @returns {Object} An object with two properties:
+ * `buffer` `ArrayBuffer`: the file content.
+ * `record` `Object`: record associated with the attachment.
+ * `_source` `String`: identifies the source of the result. Used for testing.
+ */
+ async get(
+ record,
+ options = {
+ attachmentId: record?.id,
+ }
+ ) {
+ return this.#fetchAttachment(record, {
+ ...options,
+ avoidDownload: true,
+ fallbackToCache: true,
+ fallbackToDump: true,
+ });
+ }
+
+ async #fetchAttachment(record, options) {
let {
retries,
checkHash,
attachmentId = record?.id,
fallbackToCache = false,
fallbackToDump = false,
+ avoidDownload = false,
} = options || {};
if (!attachmentId) {
// Check for pre-condition. This should not happen, but it is explicitly
@@ -195,7 +249,7 @@ export class Downloader {
// There is no local version that matches the requested record.
// Try to download the attachment specified in record.
- if (record && record.attachment) {
+ if (!avoidDownload && record && record.attachment) {
try {
const newBuffer = await this.downloadAsBytes(record, {
retries,
@@ -253,6 +307,9 @@ export class Downloader {
throw errorIfAllFails;
}
+ if (avoidDownload) {
+ throw new Downloader.NotFoundError(attachmentId);
+ }
throw new Downloader.DownloadError(attachmentId);
}
diff --git a/services/settings/IDBHelpers.sys.mjs b/services/settings/IDBHelpers.sys.mjs
index 6f2ef6937d..d77243b7a1 100644
--- a/services/settings/IDBHelpers.sys.mjs
+++ b/services/settings/IDBHelpers.sys.mjs
@@ -108,7 +108,7 @@ function executeIDB(db, storeNames, mode, callback, desc) {
desc || "execute()"
)
);
- transaction.oncomplete = event => resolve(result);
+ transaction.oncomplete = () => resolve(result);
// Simplify access to a single datastore:
if (stores.length == 1) {
stores = stores[0];
diff --git a/services/settings/RemoteSettings.worker.mjs b/services/settings/RemoteSettings.worker.mjs
index 66228f226e..bfffe9313d 100644
--- a/services/settings/RemoteSettings.worker.mjs
+++ b/services/settings/RemoteSettings.worker.mjs
@@ -140,7 +140,7 @@ let gPendingTransactions = new Set();
/**
* Import the records into the Remote Settings Chrome IndexedDB.
*
- * Note: This duplicates some logics from `kinto-offline-client.js`.
+ * Note: This duplicates some logics from `kinto-offline-client.sys.mjs`.
*
* @param {String} bucket
* @param {String} collection
diff --git a/services/settings/RemoteSettingsClient.sys.mjs b/services/settings/RemoteSettingsClient.sys.mjs
index 7e95b9baab..c521b72123 100644
--- a/services/settings/RemoteSettingsClient.sys.mjs
+++ b/services/settings/RemoteSettingsClient.sys.mjs
@@ -34,7 +34,7 @@ ChromeUtils.defineLazyGetter(lazy, "console", () => lazy.Utils.log);
function cacheProxy(target) {
const cache = new Map();
return new Proxy(target, {
- get(target, prop, receiver) {
+ get(target, prop) {
if (!cache.has(prop)) {
cache.set(prop, target[prop]);
}
diff --git a/services/settings/RemoteSettingsComponents.sys.mjs b/services/settings/RemoteSettingsComponents.sys.mjs
index 9f7687514f..1b08d66978 100644
--- a/services/settings/RemoteSettingsComponents.sys.mjs
+++ b/services/settings/RemoteSettingsComponents.sys.mjs
@@ -15,7 +15,7 @@ RemoteSettingsTimer.prototype = {
contractID: "@mozilla.org/services/settings;1",
// By default, this timer fires once every 24 hours. See the "services.settings.poll_interval" pref.
- notify(timer) {
+ notify() {
lazy.RemoteSettings.pollChanges({ trigger: "timer" }).catch(e =>
console.error(e)
);
diff --git a/services/settings/docs/index.rst b/services/settings/docs/index.rst
index fb9cdc1f49..c91d5e6ba6 100644
--- a/services/settings/docs/index.rst
+++ b/services/settings/docs/index.rst
@@ -532,12 +532,14 @@ For example, they leverage advanced customization options (bucket, content-signa
.. code-block:: js
- const {RemoteSecuritySettings} = ChromeUtils.import("resource://gre/modules/psm/RemoteSecuritySettings.jsm");
+ const {RemoteSecuritySettings} =
+ ChromeUtils.importESModule("resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs");
RemoteSecuritySettings.init();
- const {BlocklistPrivate} = ChromeUtils.import("resource://gre/modules/Blocklist.jsm");
+ const {BlocklistPrivate} =
+ ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs");
BlocklistPrivate.ExtensionBlocklistRS._ensureInitialized();
BlocklistPrivate.PluginBlocklistRS._ensureInitialized();
diff --git a/services/settings/dumps/blocklists/addons-bloomfilters.json b/services/settings/dumps/blocklists/addons-bloomfilters.json
index b7fadac629..3e8d6e5524 100644
--- a/services/settings/dumps/blocklists/addons-bloomfilters.json
+++ b/services/settings/dumps/blocklists/addons-bloomfilters.json
@@ -3,6 +3,291 @@
{
"stash": {
"blocked": [
+ "{1f7d545d-970e-49e8-acd8-834d4a9aff09}:1.1"
+ ],
+ "unblocked": []
+ },
+ "schema": 1712752562106,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1712860505827,
+ "id": "a6c8570f-b4b3-4798-8a27-494046d2e2e7",
+ "last_modified": 1712860562321
+ },
+ {
+ "stash": {
+ "blocked": [
+ "ownsolution@admingrouphelper:0.5.2",
+ "firepocket@dev:2024.1.24.1",
+ "invited@admcm:0.3.0",
+ "analyzer@appsntricks:1.2",
+ "official@traveltab:1.3",
+ "goya@flowerbag:0.1.5",
+ "umanager@tsolutions:0.1.7",
+ "official@traveltab:1.2",
+ "prod@achelper:0.3.0",
+ "invited@admcm:0.1.1",
+ "beta@agmanager:0.1.6",
+ "init@gadmininit:0.1.0",
+ "private@lmaker:0.1.1",
+ "acmanager@final:0.1.4",
+ "ownsolution@admingrouphelper:0.1.0",
+ "beta@agmanager:0.4.0",
+ "corganizer@final:0.2.1",
+ "yourmove@official:0.1.0",
+ "prod@achelper:0.1.1",
+ "listener@notfarinc:1.0.0",
+ "fivestar@fivestarpocket:0.0.3",
+ "ownsolution@admingrouphelper:0.7.0",
+ "ownsolution@admingrouphelper:0.5.4",
+ "ownsolution@admingrouphelper:0.2.0",
+ "modearn@prod:2023.11.16",
+ "umanager@tsolutions:0.2.2",
+ "ownsolution@admingrouphelper:0.5.1",
+ "official@traveltab:1.4",
+ "prodocly@main:0.0.2",
+ "acmanager@final:0.1.2",
+ "holder@cholder:0.1.5",
+ "moreapp@moreappfinal:0.0.7",
+ "beta@agmanager:0.1.2",
+ "prod@achelper:0.1.6",
+ "corganizer@final:0.1.6",
+ "corganizer@final:0.2.2",
+ "private@lmaker:0.3.0",
+ "ownsolution@admingrouphelper:0.5.7",
+ "corganizer@final:0.1.7",
+ "smasters@dev:2023.11.10.1",
+ "beta@agmanager:0.1.0",
+ "private@lmaker:0.5.0",
+ "moreapp@moreappfinal:0.0.8",
+ "monschedule@dev:2024.1.17.1",
+ "holder@cholder:0.1.7",
+ "goya@flowerbag:0.1.0",
+ "acmanager@beta:0.0.4",
+ "invited@admcm:0.4.0",
+ "ownsolution@admingrouphelper:0.5.3",
+ "init@gadmininit:0.1.7",
+ "beta@agmanager:0.1.3",
+ "corganizer@final:0.2.4",
+ "goya@flowerbag:0.0.4",
+ "debug@angelcookie.app:0.2.1",
+ "analyzer@appsntricks:1.1",
+ "malzapp@official:0.0.1",
+ "holder@cholder:0.5.0",
+ "holder@cholder:0.6.0",
+ "prod@achelper:0.1.0",
+ "holder@cholder:0.1.2",
+ "holder@cholder:0.1.0",
+ "yourmove@official:0.0.7",
+ "ownsolution@admingrouphelper:0.5.5",
+ "fivestar@fivestarpocket:0.0.1",
+ "acmanager@final:0.1.0",
+ "umanager@tsolutions:0.2.3",
+ "dexhouse@official:2024.2.16.1",
+ "holder@cholder:0.3.0",
+ "userapproved@uasystem:0.0.1",
+ "ownsolution@admingrouphelper:0.4.0",
+ "private@lmaker:0.2.0",
+ "beta@agmanager:0.1.1",
+ "modearn@prod:2023.11.15",
+ "goya@flowerbag:0.1.4",
+ "goya@flowerbag:0.0.2",
+ "umanager@tsolutions:0.1.5",
+ "invited@admcm:0.2.0",
+ "yourmove@official:0.0.6",
+ "prod@achelper:0.4.0",
+ "goya@flowerbag:0.0.5",
+ "acmanager@beta:0.0.2",
+ "goya@flowerbag:0.0.1",
+ "private@lmaker:0.1.0",
+ "beta@agmanager:0.1.4",
+ "umanager@tsolutions:0.2.0",
+ "beta@agmanager:0.2.0",
+ "fivestar@fivestarpocket:0.0.4",
+ "moreapp@moreappfinal:0.0.3",
+ "beta@agmanager:0.3.0",
+ "goya@flowerbag:0.0.6",
+ "goya@flowerbag:0.0.3",
+ "init@gadmininit:0.2.0",
+ "vertez@prod:2024.2.28.1",
+ "ownsolution@admingrouphelper:0.5.6",
+ "smasters@dev:2023.11.13.0",
+ "userapproved@uasystem:0.0.2",
+ "userapproved@uasystem:0.0.5",
+ "corganizer@final:0.1.5",
+ "goya@flowerbag:0.1.3",
+ "frypray@systems:0.0.1",
+ "holder@cholder:0.1.6",
+ "ownsolution@admingrouphelper:0.6.0",
+ "official@traveltab:1.0",
+ "official@traveltab:1.1",
+ "init@gadmininit:0.4.0",
+ "userapproved@uasystem:0.0.6",
+ "corganizer@final:0.2.0",
+ "yourmove@official:0.0.1",
+ "userapproved@uasystem:0.0.4",
+ "ownsolution@admingrouphelper:0.8.0",
+ "ownsolution@admingrouphelper:0.5.0",
+ "umanager@tsolutions:0.1.0",
+ "goya@flowerbag:0.1.2",
+ "moreapp@moreappfinal:0.0.5",
+ "invited@admcm:0.1.8",
+ "private@lmaker:0.6.0",
+ "corganizer@final:0.1.0",
+ "yourmove@official:0.0.5",
+ "yourmove@official:0.0.2",
+ "prodocly@main:0.0.1",
+ "moreapp@moreappfinal:0.0.1",
+ "yourmove@official:0.0.8",
+ "moreapp@moreappfinal:0.0.2",
+ "goya@flowerbag:0.0.9",
+ "moreapp@moreappfinal:0.0.6",
+ "init@gadmininit:0.1.5",
+ "acmanager@beta:0.0.1",
+ "umanager@tsolutions:0.2.1",
+ "prod@achelper:0.1.5",
+ "beta@agmanager:0.1.7",
+ "yourmove@official:0.0.4",
+ "private@lmaker:0.4.0",
+ "goya@flowerbag:0.1.1",
+ "invited@admcm:0.1.0",
+ "acmanager@beta:0.0.3",
+ "holder@cholder:0.2.0",
+ "listener@notfarinc:1.0.1",
+ "userapproved@uasystem:0.0.3",
+ "goya@flowerbag:0.0.8",
+ "goya@flowerbag:0.0.7",
+ "invited@admcm:0.1.6",
+ "holder@cholder:0.4.0",
+ "holder@cholder:0.1.1",
+ "corganizer@final:0.2.3",
+ "init@gadmininit:0.1.6",
+ "acmanager@final:0.1.3",
+ "moreapp@moreappfinal:0.0.9",
+ "prod@achelper:0.2.0",
+ "ownsolution@admingrouphelper:0.3.0",
+ "prod@achelper:0.1.8",
+ "invited@admcm:0.1.7",
+ "userapproved@uasystem:0.0.7",
+ "beta@agmanager:0.1.5",
+ "yourmove@official:0.0.3",
+ "moreapp@moreappfinal:0.0.4",
+ "init@gadmininit:0.1.1",
+ "userapproved@uasystem:1.1.9",
+ "userapproved@uasystem:1.1.8",
+ "invited@admcm:0.1.5",
+ "bmttools@prod:0.0.1",
+ "analyzer@appsntricks:1.0",
+ "init@gadmininit:0.3.0",
+ "fivestar@fivestarpocket:0.0.2",
+ "umanager@tsolutions:0.1.6",
+ "prod@achelper:0.1.7"
+ ],
+ "unblocked": []
+ },
+ "schema": 1712744602348,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1712752505697,
+ "id": "8a549832-9ac4-4cfd-ab1b-190da763c3f4",
+ "last_modified": 1712752561961
+ },
+ {
+ "stash": {
+ "blocked": [
+ "{bffa7976-db34-42e3-8d80-17acee672804}:19",
+ "{8858e73c-8617-402e-b57d-de5fcfdcd26a}:1.4"
+ ],
+ "unblocked": []
+ },
+ "schema": 1712342161525,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1712601304195,
+ "id": "6e3e7b5a-0bc5-43f1-87fd-96e397f0bbca",
+ "last_modified": 1712601358306
+ },
+ {
+ "stash": {
+ "blocked": [
+ "premiumblocker@addon:1.0",
+ "premiumblocker@addon:1.2",
+ "premiumblocker@addon:1.1"
+ ],
+ "unblocked": []
+ },
+ "schema": 1712314258197,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1712342104382,
+ "id": "928afd6e-3aec-466b-8c59-bcfd8007ff13",
+ "last_modified": 1712342161400
+ },
+ {
+ "stash": {
+ "blocked": [
+ "{7f72ddf5-1bd4-4515-81ca-82d58e0bf6b2}:1.0.0",
+ "{33bd1745-0a29-41b6-84a7-3029a3fe3719}:1.1.15",
+ "{7f72ddf5-1bd4-4515-81ca-82d58e0bf6b2}:1.0.2",
+ "{b171cb50-6590-4718-92a1-42dc50197e5d}:0.0.1",
+ "{17942f1e-9956-4191-a6a7-3c003ef8624a}:1.0",
+ "{8ceab6f8-b954-439b-b7e2-52ec58472842}:1.1.7",
+ "{3e9f3c3b-b14d-417b-9933-44f028e85a07}:2.1.0",
+ "{7f72ddf5-1bd4-4515-81ca-82d58e0bf6b2}:1.0.1",
+ "{5bb57a62-2c46-4f87-bc69-7f731f277250}:1.6",
+ "{dc72d0ad-67d1-4199-be96-9ee05ac2cc1e}:3.0",
+ "{55e16062-37e8-404f-a7cb-51c34f29190a}:3.2",
+ "{e24795ad-5cb8-4d59-81fd-f095a7d86725}:1.2.11",
+ "{33bd1745-0a29-41b6-84a7-3029a3fe3719}:1.1.14",
+ "{8ceab6f8-b954-439b-b7e2-52ec58472842}:1.1.8",
+ "{e24795ad-5cb8-4d59-81fd-f095a7d86725}:1.2.10"
+ ],
+ "unblocked": []
+ },
+ "schema": 1711391760286,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1711564504509,
+ "id": "1cd17f74-8694-4314-b217-c69557882c51",
+ "last_modified": 1711564561309
+ },
+ {
+ "stash": {
+ "blocked": [
+ "{5989dbf0-76bc-4ab2-9be4-a41c7ec054cb}:1.0",
+ "{e8526ae3-f86a-49f6-80e5-3a510b63b0f2}:1.0",
+ "{7f9d3107-5f02-4d08-b4c9-490d0ec2b27f}:1.0",
+ "{03f4624c-1f68-40d6-b35d-a9dce59ddd68}:1.0.0",
+ "{dda3cd29-b4a8-4d4a-b131-cfab6e8727aa}:1.2",
+ "{1aec60c2-46e6-4d7e-946d-f91b215db084}:1.0.0",
+ "{2883de72-c62b-4525-a81a-8acb7c3767a4}:1.0",
+ "{1aec60c2-46e6-4d7e-946d-f91b215db084}:1.1.0",
+ "{0cd74264-5b96-4e4e-9df2-c19735f4936b}:1.0",
+ "{b2439207-035f-4def-b9e8-2a9a17c2d6d2}:1.2",
+ "{101ea9e3-7eef-4cbd-bb3b-f6d6707557dc}:1.0",
+ "{5c365316-637d-4516-b668-60ce301bd0d5}:1.0",
+ "{3874328b-dbb8-4971-84ef-364fe214f432}:1.1.0",
+ "{805ac85d-ce15-4725-ba4e-96290ac94516}:1.1.0"
+ ],
+ "unblocked": []
+ },
+ "schema": 1711046158915,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1711391704548,
+ "id": "fd45ebfd-135f-48c8-a9e3-d8b09c85560e",
+ "last_modified": 1711391760159
+ },
+ {
+ "stash": {
+ "blocked": [
+ "{557505af-170c-424d-887f-fce817abd62b}:1.4"
+ ],
+ "unblocked": []
+ },
+ "schema": 1711039011310,
+ "key_format": "{guid}:{version}",
+ "stash_time": 1711046104269,
+ "id": "45d7d239-9fe6-4583-9658-a222ddab91b3",
+ "last_modified": 1711046158801
+ },
+ {
+ "stash": {
+ "blocked": [
"{c897d01a-2f32-4d90-b583-0db861a92a4d}:1.3"
],
"unblocked": []
@@ -239,5 +524,5 @@
"last_modified": 1707395854769
}
],
- "timestamp": 1710938159550
+ "timestamp": 1712860562321
}
diff --git a/services/settings/dumps/gen_last_modified.py b/services/settings/dumps/gen_last_modified.py
index d03f8fc096..d16eaa01e3 100644
--- a/services/settings/dumps/gen_last_modified.py
+++ b/services/settings/dumps/gen_last_modified.py
@@ -52,6 +52,7 @@ def main(output):
assert buildconfig.substs["MOZ_BUILD_APP"] in (
"browser",
"mobile/android",
+ "mobile/ios",
"comm/mail",
"comm/suite",
), (
@@ -64,6 +65,8 @@ def main(output):
dumps_locations += ["services/settings/dumps/"]
elif buildconfig.substs["MOZ_BUILD_APP"] == "mobile/android":
dumps_locations += ["services/settings/dumps/"]
+ elif buildconfig.substs["MOZ_BUILD_APP"] == "mobile/ios":
+ dumps_locations += ["services/settings/dumps/"]
elif buildconfig.substs["MOZ_BUILD_APP"] == "comm/mail":
dumps_locations += ["services/settings/dumps/"]
dumps_locations += ["comm/mail/app/settings/dumps/"]
diff --git a/services/settings/dumps/main/cookie-banner-rules-list.json b/services/settings/dumps/main/cookie-banner-rules-list.json
index 602b34a4b9..5d1c71818f 100644
--- a/services/settings/dumps/main/cookie-banner-rules-list.json
+++ b/services/settings/dumps/main/cookie-banner-rules-list.json
@@ -2,20 +2,518 @@
"data": [
{
"click": {
+ "optIn": "#c-bns #c-p-bn",
+ "optOut": "#c-bns button.grey",
+ "presence": "#cm"
+ },
+ "schema": 1711039008141,
+ "cookies": {
+ "optOut": [
+ {
+ "name": "cc_cookie",
+ "value": "{\"level\":[\"necessary\"],\"revision\":0,\"data\":null,\"rfc_cookie\":false}"
+ }
+ ]
+ },
+ "domains": [
+ "yazio.com"
+ ],
+ "id": "B8CB497B-9E12-49A7-BA2A-B7842CAEDFC3",
+ "last_modified": 1711550955136
+ },
+ {
+ "click": {
+ "optIn": "#rgpd-btn-index-accept",
+ "optOut": "#rgpd-btn-index-continue",
+ "presence": "#modalTpl"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "rueducommerce.fr"
+ ],
+ "id": "BC3582FC-C5FA-4743-85E8-7E46F67629AB",
+ "last_modified": 1711550955133
+ },
+ {
+ "click": {
+ "optIn": "#cookieConsentAcceptButton",
+ "optOut": "#cookieConsentRefuseButton",
+ "presence": "#cookieConsentBanner"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "ldlc.com",
+ "ldlc.pro",
+ "materiel.net"
+ ],
+ "id": "4A767353-98B9-4284-857A-D98DC3ECDFE1",
+ "last_modified": 1711550955129
+ },
+ {
+ "click": {
+ "optIn": "#header_tc_privacy_button_3",
+ "optOut": "#header_tc_privacy_button",
+ "presence": "#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "ovh.com",
+ "ovhcloud.com",
+ "ovhtelecom.fr"
+ ],
+ "id": "17B1F270-F499-451C-AED5-5C737106F003",
+ "last_modified": 1711550955125
+ },
+ {
+ "click": {
+ "optIn": "#popin_tc_privacy_button_2",
+ "optOut": "#optout_link",
+ "presence": "#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "laredoute.fr"
+ ],
+ "id": "9022E9FE-2DC2-48DD-BE4A-EFA8A2C81E2B",
+ "last_modified": 1711550955121
+ },
+ {
+ "click": {
+ "optIn": "#popin_tc_privacy_button_2",
+ "optOut": "#popin_tc_privacy_button",
+ "presence": "#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "quechoisir.org",
+ "quechoisirensemble.fr"
+ ],
+ "id": "5D50AA8D-D00E-4A51-8D32-64965A0575CA",
+ "last_modified": 1711550955117
+ },
+ {
+ "click": {
+ "optIn": "button#footer_tc_privacy_button_3",
+ "optOut": "button#footer_tc_privacy_button_2",
+ "presence": "div#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "labanquepostale.fr"
+ ],
+ "id": "6c1ebd2b-867a-40a5-8184-0ead733eae69",
+ "last_modified": 1711550955113
+ },
+ {
+ "click": {
+ "optIn": "button.orejime-Notice-saveButton",
+ "optOut": "button.orejime-Notice-declineButton",
+ "presence": "div.orejime-Notice"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "service-public.fr"
+ ],
+ "id": "7293dc4c-1d3d-4236-84a6-3c5cb3def55a",
+ "last_modified": 1711550955109
+ },
+ {
+ "schema": 1711039008141,
+ "cookies": {
+ "optIn": [
+ {
+ "name": "cb",
+ "value": "1_2055_07_11_"
+ }
+ ],
+ "optOut": [
+ {
+ "name": "cb",
+ "value": "1_2055_07_11_2-3"
+ }
+ ]
+ },
+ "domains": [
+ "threads.net"
+ ],
+ "id": "c232eab8-f55a-436a-8033-478746d05d98",
+ "last_modified": 1711550955105
+ },
+ {
+ "click": {
+ "optIn": "button#onetrust-accept-btn-handler",
+ "optOut": ".ot-pc-refuse-all-handler, #onetrust-reject-all-handler",
+ "presence": "div#onetrust-consent-sdk"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "espncricinfo.com",
+ "blackboard.com",
+ "roche.com",
+ "apnews.com",
+ "nationalgeographic.com",
+ "espn.com",
+ "hotjar.com",
+ "marriott.com",
+ "hootsuite.com",
+ "wattpad.com",
+ "gamespot.com",
+ "apa.org",
+ "opendns.com",
+ "epicgames.com",
+ "zendesk.com",
+ "drei.at",
+ "ikea.com",
+ "search.ch",
+ "centrum.sk",
+ "zoom.us",
+ "pluska.sk",
+ "hp.com",
+ "ceskatelevize.cz",
+ "telenet.be",
+ "adobe.com",
+ "rottentomatoes.com",
+ "dhl.com",
+ "dhl.de",
+ "nvidia.com",
+ "cloudflare.com",
+ "webex.com",
+ "indeed.com",
+ "discord.com",
+ "sport.ro",
+ "ricardo.ch",
+ "stirileprotv.ro",
+ "1177.se",
+ "meinbezirk.at",
+ "orange.ro",
+ "ica.se",
+ "flashscore.pl",
+ "kuleuven.be",
+ "tutti.ch",
+ "post.at",
+ "rezultati.com",
+ "nbg.gr",
+ "behance.net",
+ "zemanta.com",
+ "grammarly.com",
+ "usatoday.com",
+ "cnet.com",
+ "npr.org",
+ "binance.com",
+ "linktr.ee",
+ "time.com",
+ "cisco.com",
+ "udemy.com",
+ "shutterstock.com",
+ "investopedia.com",
+ "cbsnews.com",
+ "okta.com",
+ "appsflyer.com",
+ "typepad.com",
+ "calendly.com",
+ "verisign.com",
+ "outbrain.com",
+ "zdnet.com",
+ "deloitte.com",
+ "hdfcbank.com",
+ "media.net",
+ "docker.com",
+ "avast.com",
+ "bluehost.com",
+ "nba.com",
+ "hostgator.com",
+ "scientificamerican.com",
+ "aljazeera.com",
+ "sahibinden.com",
+ "rackspace.com",
+ "namecheap.com",
+ "people.com",
+ "branch.io",
+ "tv2.dk",
+ "criteo.com",
+ "trustpilot.com",
+ "hm.com",
+ "mailchimp.com",
+ "surveymonkey.com",
+ "mckinsey.com",
+ "rollingstone.com",
+ "slate.com",
+ "dictionary.com",
+ "coursera.org",
+ "msn.com",
+ "chegg.com",
+ "variety.com",
+ "cnn.com",
+ "proximus.be",
+ "adevarul.ro",
+ "cnbc.com",
+ "oe24.at",
+ "reuters.com",
+ "booking.com",
+ "bluewin.ch",
+ "viaplay.dk",
+ "aib.ie",
+ "hbomax.com",
+ "rtlnieuws.nl",
+ "buienradar.be",
+ "viaplay.se",
+ "antena3.ro",
+ "statista.com",
+ "pixabay.com",
+ "constantcontact.com",
+ "atlassian.com",
+ "bmj.com",
+ "trendyol.com",
+ "meetup.com",
+ "vmware.com",
+ "bitbucket.org",
+ "viaplay.no",
+ "asana.com",
+ "freepik.com",
+ "heute.at",
+ "mtvuutiset.fi",
+ "buienradar.nl",
+ "nypost.com",
+ "panasonic.com",
+ "safeway.com",
+ "amd.com",
+ "atg.se",
+ "brother.de",
+ "brother.eu",
+ "brother.fr",
+ "corsair.com",
+ "crucial.com",
+ "dc.com",
+ "dn.no",
+ "epson.de",
+ "epson.es",
+ "epson.eu",
+ "epson.fr",
+ "epson.it",
+ "evga.com",
+ "fortnite.com",
+ "fujitsu.com",
+ "global.canon",
+ "gpuopen.com",
+ "info.lidl",
+ "inpost.es",
+ "inpost.eu",
+ "inpost.it",
+ "intel.com",
+ "kaufland.de",
+ "lg.com",
+ "lidl.co.uk",
+ "lidl.com",
+ "lidl.cz",
+ "lidl.de",
+ "lidl.fr",
+ "lidl.it",
+ "lidl.pl",
+ "lifewire.com",
+ "logitech.com",
+ "micron.com",
+ "mythomson.com",
+ "oki.com",
+ "otto.de",
+ "razer.com",
+ "rightmove.co.uk",
+ "sbb.ch",
+ "seagate.com",
+ "soundcloud.com",
+ "trello.com",
+ "unrealengine.com",
+ "askubuntu.com",
+ "mathoverflow.net",
+ "serverfault.com",
+ "stackapps.com",
+ "stackexchange.com",
+ "stackoverflow.com",
+ "superuser.com",
+ "carrefour.fr"
+ ],
+ "id": "6c7366a0-4762-47b9-8eeb-04e86cc7a0cc",
+ "last_modified": 1711550955100
+ },
+ {
+ "click": {
+ "optIn": "#footer_tc_privacy_button",
+ "optOut": "#footer_tc_privacy_button_2",
+ "presence": ".tc-privacy-wrapper.tc-privacy-override.tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "arte.tv",
+ "urssaf.fr"
+ ],
+ "id": "cc818b41-7b46-46d3-9b17-cf924cbe87d1",
+ "last_modified": 1711550955095
+ },
+ {
+ "click": {
+ "optIn": "button#didomi-notice-agree-button",
+ "optOut": "span.didomi-continue-without-agreeing",
+ "presence": "div#didomi-popup"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "theconversation.com",
+ "leparisien.fr",
+ "lesechos.fr",
+ "numerama.com",
+ "jofogas.hu",
+ "orange.fr",
+ "meteofrance.com",
+ "subito.it",
+ "hasznaltauto.hu",
+ "zdnet.de",
+ "intersport.fr",
+ "decathlon.fr",
+ "leboncoin.fr",
+ "boursorama.com",
+ "boursobank.com",
+ "intermarche.com",
+ "bricomarche.com",
+ "entrepot-du-bricolage.fr",
+ "lesnumeriques.com",
+ "seloger.com",
+ "societe.com",
+ "manomano.fr",
+ "pagesjaunes.fr",
+ "sncf-connect.com",
+ "largus.fr"
+ ],
+ "id": "c1d7be10-151e-4a66-b83b-31a762869a97",
+ "last_modified": 1711550955088
+ },
+ {
+ "click": {
+ "optIn": "button#footer_tc_privacy_button_2",
+ "optOut": "button#footer_tc_privacy_button_3",
+ "presence": "div#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "cdiscount.com",
+ "laposte.net",
+ "laposte.fr"
+ ],
+ "id": "1871561d-65f8-4972-8e8a-84fa9eb704b4",
+ "last_modified": 1711550955084
+ },
+ {
+ "click": {
+ "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
+ "optOut": "button#CybotCookiebotDialogBodyButtonDecline",
+ "presence": "div#CybotCookiebotDialog"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "politiken.dk"
+ ],
+ "id": "1006f951-cd51-47cc-9527-5036cef4b85a",
+ "last_modified": 1711550955080
+ },
+ {
+ "click": {
+ "optIn": "button#didomi-notice-agree-button",
+ "optOut": "button#didomi-notice-disagree-button",
+ "presence": "div#didomi-host"
+ },
+ "schema": 1711039008141,
+ "cookies": {},
+ "domains": [
+ "doctolib.fr",
+ "pravda.sk",
+ "topky.sk",
+ "zoznam.sk",
+ "tvnoviny.sk",
+ "aukro.cz",
+ "krone.at",
+ "cas.sk",
+ "heureka.sk",
+ "free.fr",
+ "markiza.sk",
+ "willhaben.at",
+ "francetvinfo.fr",
+ "france.tv",
+ "france24.com",
+ "opodo.at",
+ "opodo.ch",
+ "opodo.co.uk",
+ "opodo.de",
+ "opodo.dk",
+ "opodo.fi",
+ "opodo.fr",
+ "opodo.it",
+ "opodo.nl",
+ "opodo.no",
+ "opodo.pl",
+ "opodo.pt",
+ "radiofrance.fr",
+ "rfi.fr",
+ "blablacar.fr",
+ "6play.fr"
+ ],
+ "id": "690aa076-4a8b-48ec-b52c-1443d44ff008",
+ "last_modified": 1711550955075
+ },
+ {
+ "click": {
"optIn": "button#onetrust-accept-btn-handler",
"optOut": "button.onetrust-close-btn-handler",
"presence": "div#onetrust-consent-sdk"
},
- "schema": 1710174339269,
+ "schema": 1711039008141,
"cookies": {},
"domains": [
+ "e.leclerc",
"fnac.be",
"fnac.ch",
"fnac.com",
- "fnac.pt"
+ "fnac.pt",
+ "leclercdrive.fr",
+ "mondialrelay.fr"
],
"id": "2d821158-5945-4134-a078-56c6da4f678d",
- "last_modified": 1710331175432
+ "last_modified": 1711550955071
+ },
+ {
+ "click": {
+ "optIn": "#popin_tc_privacy_button",
+ "optOut": "#popin_tc_privacy_button_3",
+ "presence": "#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "dpd.com",
+ "dpdgroup.com",
+ "edf.fr",
+ "totalenergies.fr"
+ ],
+ "id": "43ad2b6b-a57b-4f7a-9d76-e32e696ddc10",
+ "last_modified": 1711550955067
+ },
+ {
+ "click": {
+ "optIn": "#popin_tc_privacy_button_3",
+ "optOut": "#popin_tc_privacy_button_2",
+ "presence": "#tc-privacy-wrapper"
+ },
+ "schema": 1711039008141,
+ "domains": [
+ "bouyguestelecom.fr",
+ "enedis.fr",
+ "fortuneo.fr",
+ "lcl.fr",
+ "tf1.fr"
+ ],
+ "id": "98D89E26-F4B6-4C2D-BABF-4295B922E433",
+ "last_modified": 1711550955063
},
{
"click": {
@@ -47,20 +545,6 @@
{
"click": {
"optIn": "#popin_tc_privacy_button",
- "optOut": "#popin_tc_privacy_button_3",
- "presence": "#tc-privacy-wrapper"
- },
- "schema": 1710174339269,
- "domains": [
- "dpd.com",
- "dpdgroup.com"
- ],
- "id": "43ad2b6b-a57b-4f7a-9d76-e32e696ddc10",
- "last_modified": 1710331175424
- },
- {
- "click": {
- "optIn": "#popin_tc_privacy_button",
"optOut": "#popin_tc_privacy_button_2",
"presence": "#tc-privacy-wrapper"
},
@@ -73,20 +557,6 @@
},
{
"click": {
- "optIn": "#popin_tc_privacy_button_3",
- "optOut": "#popin_tc_privacy_button_2",
- "presence": "#tc-privacy-wrapper"
- },
- "schema": 1710174339269,
- "domains": [
- "fortuneo.fr",
- "lcl.fr"
- ],
- "id": "98D89E26-F4B6-4C2D-BABF-4295B922E433",
- "last_modified": 1710331175416
- },
- {
- "click": {
"optIn": ".ei_btn_typ_validate",
"optOut": ".ei_lb_btnskip",
"presence": "#cookieLB"
@@ -316,35 +786,6 @@
},
{
"click": {
- "optIn": "button#didomi-notice-agree-button",
- "optOut": "span.didomi-continue-without-agreeing",
- "presence": "div#didomi-popup"
- },
- "schema": 1710174339269,
- "cookies": {},
- "domains": [
- "theconversation.com",
- "leparisien.fr",
- "lesechos.fr",
- "numerama.com",
- "jofogas.hu",
- "orange.fr",
- "meteofrance.com",
- "subito.it",
- "hasznaltauto.hu",
- "zdnet.de",
- "intersport.fr",
- "decathlon.fr",
- "leboncoin.fr",
- "boursorama.com",
- "boursobank.com",
- "intermarche.com"
- ],
- "id": "c1d7be10-151e-4a66-b83b-31a762869a97",
- "last_modified": 1710331175364
- },
- {
- "click": {
"optOut": ".sp_choice_type_13",
"presence": ".message-container > #notice",
"runContext": "child"
@@ -417,46 +858,6 @@
},
{
"click": {
- "optIn": "button#didomi-notice-agree-button",
- "optOut": "button#didomi-notice-disagree-button",
- "presence": "div#didomi-host"
- },
- "schema": 1710174339269,
- "cookies": {},
- "domains": [
- "doctolib.fr",
- "pravda.sk",
- "topky.sk",
- "zoznam.sk",
- "tvnoviny.sk",
- "aukro.cz",
- "krone.at",
- "cas.sk",
- "heureka.sk",
- "free.fr",
- "markiza.sk",
- "willhaben.at",
- "francetvinfo.fr",
- "france24.com",
- "opodo.at",
- "opodo.ch",
- "opodo.co.uk",
- "opodo.de",
- "opodo.dk",
- "opodo.fi",
- "opodo.fr",
- "opodo.it",
- "opodo.nl",
- "opodo.no",
- "opodo.pl",
- "opodo.pt",
- "radiofrance.fr"
- ],
- "id": "690aa076-4a8b-48ec-b52c-1443d44ff008",
- "last_modified": 1710331175349
- },
- {
- "click": {
"optOut": "button[data-js-item=\"privacy-protection-default\"]",
"presence": ".c-privacy-protection-banner"
},
@@ -675,201 +1076,6 @@
},
{
"click": {
- "optIn": "button#onetrust-accept-btn-handler",
- "optOut": ".ot-pc-refuse-all-handler, #onetrust-reject-all-handler",
- "presence": "div#onetrust-consent-sdk"
- },
- "schema": 1708699541450,
- "cookies": {},
- "domains": [
- "espncricinfo.com",
- "blackboard.com",
- "roche.com",
- "apnews.com",
- "nationalgeographic.com",
- "espn.com",
- "hotjar.com",
- "marriott.com",
- "hootsuite.com",
- "wattpad.com",
- "gamespot.com",
- "apa.org",
- "opendns.com",
- "epicgames.com",
- "zendesk.com",
- "drei.at",
- "ikea.com",
- "search.ch",
- "centrum.sk",
- "zoom.us",
- "pluska.sk",
- "hp.com",
- "ceskatelevize.cz",
- "telenet.be",
- "adobe.com",
- "rottentomatoes.com",
- "dhl.com",
- "dhl.de",
- "nvidia.com",
- "cloudflare.com",
- "webex.com",
- "indeed.com",
- "discord.com",
- "sport.ro",
- "ricardo.ch",
- "stirileprotv.ro",
- "1177.se",
- "meinbezirk.at",
- "orange.ro",
- "ica.se",
- "flashscore.pl",
- "kuleuven.be",
- "tutti.ch",
- "post.at",
- "rezultati.com",
- "nbg.gr",
- "behance.net",
- "zemanta.com",
- "grammarly.com",
- "usatoday.com",
- "cnet.com",
- "npr.org",
- "binance.com",
- "linktr.ee",
- "time.com",
- "cisco.com",
- "udemy.com",
- "shutterstock.com",
- "investopedia.com",
- "cbsnews.com",
- "okta.com",
- "appsflyer.com",
- "typepad.com",
- "calendly.com",
- "verisign.com",
- "outbrain.com",
- "zdnet.com",
- "deloitte.com",
- "hdfcbank.com",
- "media.net",
- "docker.com",
- "avast.com",
- "bluehost.com",
- "nba.com",
- "hostgator.com",
- "scientificamerican.com",
- "aljazeera.com",
- "sahibinden.com",
- "rackspace.com",
- "namecheap.com",
- "people.com",
- "branch.io",
- "tv2.dk",
- "criteo.com",
- "trustpilot.com",
- "hm.com",
- "mailchimp.com",
- "surveymonkey.com",
- "mckinsey.com",
- "rollingstone.com",
- "slate.com",
- "dictionary.com",
- "coursera.org",
- "msn.com",
- "chegg.com",
- "variety.com",
- "cnn.com",
- "proximus.be",
- "adevarul.ro",
- "cnbc.com",
- "oe24.at",
- "reuters.com",
- "booking.com",
- "bluewin.ch",
- "viaplay.dk",
- "aib.ie",
- "hbomax.com",
- "rtlnieuws.nl",
- "buienradar.be",
- "viaplay.se",
- "antena3.ro",
- "statista.com",
- "pixabay.com",
- "constantcontact.com",
- "atlassian.com",
- "bmj.com",
- "trendyol.com",
- "meetup.com",
- "vmware.com",
- "bitbucket.org",
- "viaplay.no",
- "asana.com",
- "freepik.com",
- "heute.at",
- "mtvuutiset.fi",
- "buienradar.nl",
- "nypost.com",
- "panasonic.com",
- "safeway.com",
- "amd.com",
- "atg.se",
- "brother.de",
- "brother.eu",
- "brother.fr",
- "corsair.com",
- "crucial.com",
- "dc.com",
- "dn.no",
- "epson.de",
- "epson.es",
- "epson.eu",
- "epson.fr",
- "epson.it",
- "evga.com",
- "fortnite.com",
- "fujitsu.com",
- "global.canon",
- "gpuopen.com",
- "info.lidl",
- "inpost.es",
- "inpost.eu",
- "inpost.it",
- "intel.com",
- "kaufland.de",
- "lg.com",
- "lidl.co.uk",
- "lidl.com",
- "lidl.cz",
- "lidl.de",
- "lidl.fr",
- "lidl.it",
- "lidl.pl",
- "lifewire.com",
- "logitech.com",
- "micron.com",
- "mythomson.com",
- "oki.com",
- "otto.de",
- "razer.com",
- "rightmove.co.uk",
- "sbb.ch",
- "seagate.com",
- "soundcloud.com",
- "trello.com",
- "unrealengine.com",
- "askubuntu.com",
- "mathoverflow.net",
- "serverfault.com",
- "stackapps.com",
- "stackexchange.com",
- "stackoverflow.com",
- "superuser.com"
- ],
- "id": "6c7366a0-4762-47b9-8eeb-04e86cc7a0cc",
- "last_modified": 1708772697220
- },
- {
- "click": {
"optIn": ".cc-btn.cc-allow",
"optOut": ".cc-btn.cc-deny",
"presence": ".cc-window.cc-banner"
@@ -2663,27 +2869,6 @@
},
{
"click": {
- "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
- "optOut": "button#CybotCookiebotDialogBodyButtonDecline",
- "presence": "div#CybotCookiebotDialog"
- },
- "schema": 1700784006705,
- "cookies": {
- "optOut": [
- {
- "name": "CookieConsent",
- "value": "{stamp:%277wlvZfVgG/Pfkj1y6Hfoz636NePkdUWVOV+lQLpJefS1O2ny+RqIdg==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cmethod:%27explicit%27%2Cver:5%2Cutc:1699462925195%2Ciab2:%27CP07BsAP07BsACGABBENDeCgAAAAAH_AAAAAAAAS4gGgAgABkAEQAJ4AbwA_AEWAJ2AYoBMgC8wGCAS4AAAA.YAAAAAAAAAAA%27%2Cgacm:%271~%27%2Cregion:%27de%27}"
- }
- ]
- },
- "domains": [
- "politiken.dk"
- ],
- "id": "1006f951-cd51-47cc-9527-5036cef4b85a",
- "last_modified": 1700826062547
- },
- {
- "click": {
"optIn": "#iubenda-cs-accept-btn iubenda-cs-btn-primary",
"presence": "div#iubenda-cs-banner"
},
@@ -3234,41 +3419,6 @@
},
{
"click": {
- "optIn": "#footer_tc_privacy_button",
- "optOut": "#footer_tc_privacy_button_2",
- "presence": ".tc-privacy-wrapper.tc-privacy-override.tc-privacy-wrapper"
- },
- "schema": 1690070404721,
- "domains": [
- "arte.tv"
- ],
- "id": "cc818b41-7b46-46d3-9b17-cf924cbe87d1",
- "last_modified": 1690359097318
- },
- {
- "schema": 1690070404721,
- "cookies": {
- "optIn": [
- {
- "name": "cb",
- "value": "1_2055_07_11_"
- }
- ],
- "optOut": [
- {
- "name": "cb",
- "value": "1_2055_07_11_2-3"
- }
- ]
- },
- "domains": [
- "threads.net"
- ],
- "id": "C232EAB8-F55A-436A-8033-478746D05D98",
- "last_modified": 1690359097314
- },
- {
- "click": {
"optIn": "#didomi-notice-agree-button",
"presence": "#didomi-popup"
},
@@ -9069,20 +9219,6 @@
},
{
"click": {
- "optIn": "button#footer_tc_privacy_button_2",
- "optOut": "button#footer_tc_privacy_button_3",
- "presence": "div#tc-privacy-wrapper"
- },
- "schema": 1670460541263,
- "cookies": {},
- "domains": [
- "cdiscount.com"
- ],
- "id": "1871561d-65f8-4972-8e8a-84fa9eb704b4",
- "last_modified": 1670498155756
- },
- {
- "click": {
"optIn": "button#accept-cookies",
"optOut": "button#decline-cookies",
"presence": "div#cookie-popup"
@@ -9230,5 +9366,5 @@
"last_modified": 1670498155641
}
],
- "timestamp": 1710331175432
+ "timestamp": 1711550955136
}
diff --git a/services/settings/dumps/main/devtools-compatibility-browsers.json b/services/settings/dumps/main/devtools-compatibility-browsers.json
index 013e451d25..6cf0847350 100644
--- a/services/settings/dumps/main/devtools-compatibility-browsers.json
+++ b/services/settings/dumps/main/devtools-compatibility-browsers.json
@@ -1,6 +1,141 @@
{
"data": [
{
+ "name": "Deno",
+ "schema": 1712707507752,
+ "status": "current",
+ "version": "1.42",
+ "browserid": "deno",
+ "id": "a308f3c2-cc19-4a1e-825f-898696606328",
+ "last_modified": 1712840749681
+ },
+ {
+ "name": "Edge",
+ "schema": 1712707507922,
+ "status": "planned",
+ "version": "126",
+ "browserid": "edge",
+ "id": "c8bf4918-03b7-4be2-bf75-4d6139dbd7c9",
+ "last_modified": 1712840749678
+ },
+ {
+ "name": "Safari",
+ "schema": 1712707507975,
+ "status": "beta",
+ "version": "17.5",
+ "browserid": "safari",
+ "id": "24e30aff-fbf8-4a96-a036-84f970447d4b",
+ "last_modified": 1712840749675
+ },
+ {
+ "name": "Safari on iOS",
+ "schema": 1712707508032,
+ "status": "beta",
+ "version": "17.5",
+ "browserid": "safari_ios",
+ "id": "4375a82d-f2f1-4883-8da7-0aedfb05b8f9",
+ "last_modified": 1712840749673
+ },
+ {
+ "name": "Edge",
+ "schema": 1712707507808,
+ "status": "beta",
+ "version": "124",
+ "browserid": "edge",
+ "id": "3837dc37-38b7-483b-82b3-c5593e7a4c91",
+ "last_modified": 1712840749668
+ },
+ {
+ "name": "Edge",
+ "schema": 1712707507867,
+ "status": "nightly",
+ "version": "125",
+ "browserid": "edge",
+ "id": "f1147d5f-d690-43d0-879d-117c6ca24a16",
+ "last_modified": 1712840749665
+ },
+ {
+ "name": "Firefox",
+ "schema": 1711497908121,
+ "status": "planned",
+ "version": "127",
+ "browserid": "firefox",
+ "id": "1477a1c3-a8be-4e6f-916e-8cf8eb789e3f",
+ "last_modified": 1711547229147
+ },
+ {
+ "name": "Firefox for Android",
+ "schema": 1711497908436,
+ "status": "planned",
+ "version": "127",
+ "browserid": "firefox_android",
+ "id": "13f02b93-14c9-4ca4-937d-0a83fb7fb3a5",
+ "last_modified": 1711547229144
+ },
+ {
+ "name": "Firefox",
+ "schema": 1711497908007,
+ "status": "beta",
+ "version": "125",
+ "browserid": "firefox",
+ "id": "5ae5bd40-deb0-40c4-bba6-cd411b78ee16",
+ "last_modified": 1711547229141
+ },
+ {
+ "name": "Firefox for Android",
+ "schema": 1711497908310,
+ "status": "beta",
+ "version": "125",
+ "browserid": "firefox_android",
+ "id": "a8f570e9-574d-4193-891f-fa8e0a875388",
+ "last_modified": 1711547229139
+ },
+ {
+ "name": "Firefox for Android",
+ "schema": 1711497908249,
+ "status": "current",
+ "version": "124",
+ "browserid": "firefox_android",
+ "id": "1b3c619a-5ca8-4f5f-8f0b-57a3a27a589f",
+ "last_modified": 1711547229132
+ },
+ {
+ "name": "Firefox",
+ "schema": 1711497907952,
+ "status": "current",
+ "version": "124",
+ "browserid": "firefox",
+ "id": "7f93cadc-411f-4e31-938c-cc5bfc173f85",
+ "last_modified": 1711547229129
+ },
+ {
+ "name": "Firefox for Android",
+ "schema": 1711497908374,
+ "status": "nightly",
+ "version": "126",
+ "browserid": "firefox_android",
+ "id": "b77524e9-58dc-4196-acbd-41dddc4daea2",
+ "last_modified": 1711547229127
+ },
+ {
+ "name": "Firefox",
+ "schema": 1711497908064,
+ "status": "nightly",
+ "version": "126",
+ "browserid": "firefox",
+ "id": "70b05b0b-bbef-486c-901a-ea3221a28fc1",
+ "last_modified": 1711547229124
+ },
+ {
+ "name": "Edge",
+ "schema": 1711497907820,
+ "status": "current",
+ "version": "123",
+ "browserid": "edge",
+ "id": "d0c3d84f-8d27-455b-8746-7e25607e5b78",
+ "last_modified": 1711547229120
+ },
+ {
"name": "WebView Android",
"schema": 1710547503853,
"status": "beta",
@@ -55,33 +190,6 @@
"last_modified": 1710742064631
},
{
- "name": "Edge",
- "schema": 1710374703056,
- "status": "planned",
- "version": "125",
- "browserid": "edge",
- "id": "f1147d5f-d690-43d0-879d-117c6ca24a16",
- "last_modified": 1710422368047
- },
- {
- "name": "Edge",
- "schema": 1710174339301,
- "status": "beta",
- "version": "123",
- "browserid": "edge",
- "id": "d0c3d84f-8d27-455b-8746-7e25607e5b78",
- "last_modified": 1710422368043
- },
- {
- "name": "Edge",
- "schema": 1710374702983,
- "status": "nightly",
- "version": "124",
- "browserid": "edge",
- "id": "3837dc37-38b7-483b-82b3-c5593e7a4c91",
- "last_modified": 1710422368040
- },
- {
"name": "Safari",
"schema": 1709769903331,
"status": "current",
@@ -100,87 +208,6 @@
"last_modified": 1709801393995
},
{
- "name": "Edge",
- "schema": 1709424307170,
- "status": "current",
- "version": "122",
- "browserid": "edge",
- "id": "5ad01f1f-b345-4f24-9b88-4f456a928c76",
- "last_modified": 1709536180465
- },
- {
- "name": "Firefox",
- "schema": 1709078707232,
- "status": "planned",
- "version": "126",
- "browserid": "firefox",
- "id": "70b05b0b-bbef-486c-901a-ea3221a28fc1",
- "last_modified": 1709113112619
- },
- {
- "name": "Firefox for Android",
- "schema": 1709078707604,
- "status": "planned",
- "version": "126",
- "browserid": "firefox_android",
- "id": "b77524e9-58dc-4196-acbd-41dddc4daea2",
- "last_modified": 1709113112616
- },
- {
- "name": "Firefox",
- "schema": 1709078707068,
- "status": "beta",
- "version": "124",
- "browserid": "firefox",
- "id": "7f93cadc-411f-4e31-938c-cc5bfc173f85",
- "last_modified": 1709113112610
- },
- {
- "name": "Firefox for Android",
- "schema": 1709078707459,
- "status": "beta",
- "version": "124",
- "browserid": "firefox_android",
- "id": "1b3c619a-5ca8-4f5f-8f0b-57a3a27a589f",
- "last_modified": 1709113112607
- },
- {
- "name": "Firefox for Android",
- "schema": 1709078707371,
- "status": "current",
- "version": "123",
- "browserid": "firefox_android",
- "id": "d9b7089d-7091-4794-9051-49f794c0c854",
- "last_modified": 1709113112596
- },
- {
- "name": "Firefox",
- "schema": 1709078706998,
- "status": "current",
- "version": "123",
- "browserid": "firefox",
- "id": "2aed6f85-b118-4e91-b95a-481030c3bb78",
- "last_modified": 1709113112593
- },
- {
- "name": "Firefox for Android",
- "schema": 1709078707531,
- "status": "nightly",
- "version": "125",
- "browserid": "firefox_android",
- "id": "a8f570e9-574d-4193-891f-fa8e0a875388",
- "last_modified": 1709113112588
- },
- {
- "name": "Firefox",
- "schema": 1709078707153,
- "status": "nightly",
- "version": "125",
- "browserid": "firefox",
- "id": "5ae5bd40-deb0-40c4-bba6-cd411b78ee16",
- "last_modified": 1709113112583
- },
- {
"name": "Opera",
"schema": 1708473904223,
"status": "beta",
@@ -199,15 +226,6 @@
"last_modified": 1708507560573
},
{
- "name": "Deno",
- "schema": 1706659507818,
- "status": "current",
- "version": "1.40",
- "browserid": "deno",
- "id": "15c381ea-e194-48f1-99dc-3d8cd2ffdcfb",
- "last_modified": 1706695152746
- },
- {
"name": "Opera Android",
"schema": 1706313908456,
"status": "current",
@@ -280,5 +298,5 @@
"last_modified": 1665656484764
}
],
- "timestamp": 1710742064656
+ "timestamp": 1712840749681
}
diff --git a/services/settings/dumps/main/search-telemetry-v2.json b/services/settings/dumps/main/search-telemetry-v2.json
index 1803977187..9eb4e5d29c 100644
--- a/services/settings/dumps/main/search-telemetry-v2.json
+++ b/services/settings/dumps/main/search-telemetry-v2.json
@@ -1,18 +1,20 @@
{
"data": [
{
- "isSPA": true,
- "schema": 1707523204491,
+ "schema": 1712582517430,
"components": [
{
- "type": "ad_image_row",
+ "type": "ad_carousel",
"included": {
"parent": {
- "selector": "[data-testid='pam.container']"
+ "selector": ".adsMvCarousel"
+ },
+ "related": {
+ "selector": ".cr"
},
"children": [
{
- "selector": "[data-slide-index]",
+ "selector": ".pa_item",
"countChildren": true
}
]
@@ -20,10 +22,35 @@
},
{
"type": "ad_link",
- "included": {
+ "excluded": {
"parent": {
- "selector": "[data-testid='adResult']"
+ "selector": "aside"
}
+ },
+ "included": {
+ "parent": {
+ "selector": ".sb_adTA"
+ },
+ "children": [
+ {
+ "type": "ad_sitelink",
+ "selector": ".b_vlist2col"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_sidebar",
+ "included": {
+ "parent": {
+ "selector": "aside"
+ },
+ "children": [
+ {
+ "selector": ".pa_item, .sb_adTA",
+ "countChildren": true
+ }
+ ]
}
},
{
@@ -31,14 +58,52 @@
"topDown": true,
"included": {
"parent": {
- "selector": "._1zdrb._1cR1n"
+ "selector": "form#sb_form"
},
"related": {
- "selector": "#search-suggestions"
+ "selector": "#sw_as"
},
"children": [
{
- "selector": "input[type='search']"
+ "selector": "input[name='q']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "cookie_banner",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "div#bnp_cookie_banner"
+ },
+ "children": [
+ {
+ "selector": "button#bnp_btn_accept",
+ "eventListeners": [
+ {
+ "action": "clicked_accept",
+ "eventType": "click"
+ }
+ ]
+ },
+ {
+ "selector": "button#bnp_btn_reject",
+ "eventListeners": [
+ {
+ "action": "clicked_reject",
+ "eventType": "click"
+ }
+ ]
+ },
+ {
+ "selector": "a#bnp_btn_preference",
+ "eventListeners": [
+ {
+ "action": "clicked_more_options",
+ "eventType": "click"
+ }
+ ]
}
]
}
@@ -48,33 +113,74 @@
"default": true
}
],
+ "shoppingTab": {
+ "regexp": "^/shop?",
+ "selector": "#b-scopeListItem-shop a"
+ },
"taggedCodes": [
- "brz-moz",
- "firefoxqwant"
+ "MOZ2",
+ "MOZ4",
+ "MOZ5",
+ "MOZA",
+ "MOZB",
+ "MOZD",
+ "MOZE",
+ "MOZI",
+ "MOZL",
+ "MOZM",
+ "MOZO",
+ "MOZR",
+ "MOZT",
+ "MOZW",
+ "MOZX",
+ "MZSL01",
+ "MZSL02",
+ "MZSL03"
],
- "telemetryId": "qwant",
+ "telemetryId": "bing",
"organicCodes": [],
- "codeParamName": "client",
+ "codeParamName": "pc",
"queryParamName": "q",
+ "followOnCookies": [
+ {
+ "host": "www.bing.com",
+ "name": "SRCHS",
+ "codeParamName": "PC",
+ "extraCodePrefixes": [
+ "QBRE"
+ ],
+ "extraCodeParamName": "form"
+ }
+ ],
"queryParamNames": [
"q"
],
- "searchPageRegexp": "^https://www\\.qwant\\.com/",
- "filter_expression": "env.version|versionCompare(\"124.0a1\")>=0",
- "followOnParamNames": [],
- "defaultPageQueryParam": {
- "key": "t",
- "value": "web"
+ "domainExtraction": {
+ "ads": [
+ {
+ "method": "textContent",
+ "selectors": "#b_results .b_ad .b_attribution cite, .adsMvCarousel cite, aside cite"
+ }
+ ],
+ "nonAds": [
+ {
+ "method": "textContent",
+ "selectors": "#b_results .b_algo .b_attribution cite"
+ }
+ ]
},
+ "searchPageRegexp": "^https://www\\.bing\\.com/search",
+ "nonAdsLinkRegexps": [
+ "^https://www.bing.com/ck/a"
+ ],
"extraAdServersRegexps": [
- "^https://www\\.bing\\.com/acli?c?k",
- "^https://api\\.qwant\\.com/v3/r/"
+ "^https://www\\.bing\\.com/acli?c?k"
],
- "id": "19c434a3-d173-4871-9743-290ac92a3f6b",
- "last_modified": 1707833261849
+ "id": "e1eec461-f1f3-40de-b94b-3b670b78108c",
+ "last_modified": 1712762409389
},
{
- "schema": 1705948294201,
+ "schema": 1712243919540,
"components": [
{
"type": "ad_carousel",
@@ -94,6 +200,23 @@
}
},
{
+ "type": "ad_carousel",
+ "included": {
+ "parent": {
+ "selector": ".sh-sr__shop-result-group"
+ },
+ "related": {
+ "selector": "g-right-button, g-left-button"
+ },
+ "children": [
+ {
+ "selector": ".sh-np__click-target",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
"type": "refined_search_buttons",
"topDown": true,
"included": {
@@ -183,6 +306,44 @@
}
},
{
+ "type": "cookie_banner",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "div.spoKVd"
+ },
+ "children": [
+ {
+ "selector": "button#L2AGLb",
+ "eventListeners": [
+ {
+ "action": "clicked_accept",
+ "eventType": "click"
+ }
+ ]
+ },
+ {
+ "selector": "button#W0wltc",
+ "eventListeners": [
+ {
+ "action": "clicked_reject",
+ "eventType": "click"
+ }
+ ]
+ },
+ {
+ "selector": "button#VnjCcb",
+ "eventListeners": [
+ {
+ "action": "clicked_more_options",
+ "eventType": "click"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
"type": "ad_link",
"default": true
}
@@ -229,7 +390,11 @@
"domainExtraction": {
"ads": [
{
- "method": "data-attribute",
+ "method": "textContent",
+ "selectors": ".sh-np__seller-container"
+ },
+ {
+ "method": "dataAttribute",
"options": {
"dataAttributeKey": "dtld"
},
@@ -239,11 +404,29 @@
"nonAds": [
{
"method": "href",
- "selectors": "#rso div.g[jscontroller] > div > div > div > div a[data-usg]"
+ "options": {
+ "queryParamKey": "url",
+ "queryParamValueIsHref": true
+ },
+ "selectors": ".mnIHsc > a:first-child"
+ },
+ {
+ "method": "href",
+ "selectors": "a[jsname='UWckNb']"
+ },
+ {
+ "method": "dataAttribute",
+ "options": {
+ "dataAttributeKey": "lpage"
+ },
+ "selectors": "[data-id='mosaic'] [data-lpage]"
}
]
},
"searchPageRegexp": "^https://www\\.google\\.(?:.+)/search",
+ "ignoreLinkRegexps": [
+ "^https?://consent\\.google\\.(?:.+)/d\\?continue\\="
+ ],
"nonAdsLinkRegexps": [
"^https?://www\\.google\\.(?:.+)/url?(?:.+)&url="
],
@@ -258,8 +441,84 @@
"extraAdServersRegexps": [
"^https?://www\\.google(?:adservices)?\\.com/(?:pagead/)?aclk"
],
+ "nonAdsLinkQueryParamNames": [
+ "url"
+ ],
"id": "635a3325-1995-42d6-be09-dbe4b2a95453",
- "last_modified": 1706198445460
+ "last_modified": 1712582517281
+ },
+ {
+ "isSPA": true,
+ "schema": 1707523204491,
+ "components": [
+ {
+ "type": "ad_image_row",
+ "included": {
+ "parent": {
+ "selector": "[data-testid='pam.container']"
+ },
+ "children": [
+ {
+ "selector": "[data-slide-index]",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "included": {
+ "parent": {
+ "selector": "[data-testid='adResult']"
+ }
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "._1zdrb._1cR1n"
+ },
+ "related": {
+ "selector": "#search-suggestions"
+ },
+ "children": [
+ {
+ "selector": "input[type='search']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "taggedCodes": [
+ "brz-moz",
+ "firefoxqwant"
+ ],
+ "telemetryId": "qwant",
+ "organicCodes": [],
+ "codeParamName": "client",
+ "queryParamName": "q",
+ "queryParamNames": [
+ "q"
+ ],
+ "searchPageRegexp": "^https://www\\.qwant\\.com/",
+ "filter_expression": "env.version|versionCompare(\"124.0a1\")>=0",
+ "followOnParamNames": [],
+ "defaultPageQueryParam": {
+ "key": "t",
+ "value": "web"
+ },
+ "extraAdServersRegexps": [
+ "^https://www\\.bing\\.com/acli?c?k",
+ "^https://api\\.qwant\\.com/v3/r/"
+ ],
+ "id": "19c434a3-d173-4871-9743-290ac92a3f6b",
+ "last_modified": 1707833261849
},
{
"schema": 1705363206938,
@@ -524,134 +783,7 @@
],
"id": "9a487171-3a06-4647-8866-36250ec84f3a",
"last_modified": 1698666532324
- },
- {
- "schema": 1698656462833,
- "components": [
- {
- "type": "ad_carousel",
- "included": {
- "parent": {
- "selector": ".adsMvCarousel"
- },
- "related": {
- "selector": ".cr"
- },
- "children": [
- {
- "selector": ".pa_item",
- "countChildren": true
- }
- ]
- }
- },
- {
- "type": "ad_link",
- "excluded": {
- "parent": {
- "selector": "aside"
- }
- },
- "included": {
- "parent": {
- "selector": ".sb_adTA"
- },
- "children": [
- {
- "type": "ad_sitelink",
- "selector": ".b_vlist2col"
- }
- ]
- }
- },
- {
- "type": "ad_sidebar",
- "included": {
- "parent": {
- "selector": "aside"
- },
- "children": [
- {
- "selector": ".pa_item, .sb_adTA",
- "countChildren": true
- }
- ]
- }
- },
- {
- "type": "incontent_searchbox",
- "topDown": true,
- "included": {
- "parent": {
- "selector": "form#sb_form"
- },
- "related": {
- "selector": "#sw_as"
- },
- "children": [
- {
- "selector": "input[name='q']"
- }
- ]
- }
- },
- {
- "type": "ad_link",
- "default": true
- }
- ],
- "shoppingTab": {
- "regexp": "^/shop?",
- "selector": "#b-scopeListItem-shop a"
- },
- "taggedCodes": [
- "MOZ2",
- "MOZ4",
- "MOZ5",
- "MOZA",
- "MOZB",
- "MOZD",
- "MOZE",
- "MOZI",
- "MOZL",
- "MOZM",
- "MOZO",
- "MOZR",
- "MOZT",
- "MOZW",
- "MOZX",
- "MZSL01",
- "MZSL02",
- "MZSL03"
- ],
- "telemetryId": "bing",
- "organicCodes": [],
- "codeParamName": "pc",
- "queryParamName": "q",
- "followOnCookies": [
- {
- "host": "www.bing.com",
- "name": "SRCHS",
- "codeParamName": "PC",
- "extraCodePrefixes": [
- "QBRE"
- ],
- "extraCodeParamName": "form"
- }
- ],
- "queryParamNames": [
- "q"
- ],
- "searchPageRegexp": "^https://www\\.bing\\.com/search",
- "nonAdsLinkRegexps": [
- "^https://www.bing.com/ck/a"
- ],
- "extraAdServersRegexps": [
- "^https://www\\.bing\\.com/acli?c?k"
- ],
- "id": "e1eec461-f1f3-40de-b94b-3b670b78108c",
- "last_modified": 1698666532321
}
],
- "timestamp": 1707833261849
+ "timestamp": 1712762409389
}
diff --git a/services/settings/dumps/main/translations-models.json b/services/settings/dumps/main/translations-models.json
index f81d4b9002..7144b09988 100644
--- a/services/settings/dumps/main/translations-models.json
+++ b/services/settings/dumps/main/translations-models.json
@@ -1,6 +1,114 @@
{
"data": [
{
+ "name": "lex.50.50.encs.s2t.bin",
+ "schema": 1712262975283,
+ "toLang": "cs",
+ "version": "1.0a2",
+ "fileType": "lex",
+ "fromLang": "en",
+ "attachment": {
+ "hash": "ba42a89db077e6444e7409363a1c71165c6210c21579ad8a310d0d98b0ea9485",
+ "size": 3365784,
+ "filename": "lex.50.50.encs.s2t.bin",
+ "location": "main-workspace/translations-models/73a4a96b-88b2-4c3c-bd9e-d2e10ec89a11.bin",
+ "mimetype": "application/octet-stream"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "3ed4aa73-ff32-48d5-b546-d97d85bb9ba7",
+ "last_modified": 1712849848180
+ },
+ {
+ "name": "vocab.encs.spm",
+ "schema": 1712262976718,
+ "toLang": "cs",
+ "version": "1.0a2",
+ "fileType": "vocab",
+ "fromLang": "en",
+ "attachment": {
+ "hash": "8af48f972eeba9a9011c0525adf83e0e7abc54c328b2217351a3dad5a852040b",
+ "size": 805211,
+ "filename": "vocab.encs.spm",
+ "location": "main-workspace/translations-models/dc866438-dcf5-4376-b65c-85847f344de7.spm",
+ "mimetype": "text/plain"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "5c9d1bf3-1ad1-4a8d-bf32-f092a42d49f5",
+ "last_modified": 1712849848177
+ },
+ {
+ "name": "model.encs.intgemm.alphas.bin",
+ "schema": 1712262978134,
+ "toLang": "cs",
+ "version": "1.0a2",
+ "fileType": "model",
+ "fromLang": "en",
+ "attachment": {
+ "hash": "7815671064234ff87bc20e9f30e736a23cdcb2286fd1aea44db16116a8ab8680",
+ "size": 17140898,
+ "filename": "model.encs.intgemm.alphas.bin",
+ "location": "main-workspace/translations-models/45138d27-add3-4c27-b3ca-181fcacd515b.bin",
+ "mimetype": "application/octet-stream"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "cffe3283-7ceb-4849-a011-7560aa02a21b",
+ "last_modified": 1712849848174
+ },
+ {
+ "name": "lex.50.50.lten.s2t.bin",
+ "schema": 1712019552519,
+ "toLang": "en",
+ "version": "1.0a2",
+ "fileType": "lex",
+ "fromLang": "lt",
+ "attachment": {
+ "hash": "fcb332258413ccd775f22f6ef86e8f662274dde500541f8ef2c019040093aa9c",
+ "size": 4180224,
+ "filename": "lex.50.50.lten.s2t.bin",
+ "location": "main-workspace/translations-models/6fe59304-2a5b-4da6-bd2f-18ba4ddd1c41.bin",
+ "mimetype": "application/octet-stream"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "5109e947-b8e8-40d2-bfcf-e6a23e27b0dc",
+ "last_modified": 1712068183362
+ },
+ {
+ "name": "model.lten.intgemm.alphas.bin",
+ "schema": 1712019556899,
+ "toLang": "en",
+ "version": "1.0a2",
+ "fileType": "model",
+ "fromLang": "lt",
+ "attachment": {
+ "hash": "7a47a0602d7d3da39914ae4f4eb7985f1de0c2cf6740bdbae294456c97cdfb5d",
+ "size": 17141051,
+ "filename": "model.lten.intgemm.alphas.bin",
+ "location": "main-workspace/translations-models/2bab163d-7bc3-40d5-ad94-aeb26049bbc3.bin",
+ "mimetype": "application/octet-stream"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "60631420-3179-4eb4-aca8-da7aef59032d",
+ "last_modified": 1712068183359
+ },
+ {
+ "name": "vocab.lten.spm",
+ "schema": 1712019566702,
+ "toLang": "en",
+ "version": "1.0a2",
+ "fileType": "vocab",
+ "fromLang": "lt",
+ "attachment": {
+ "hash": "6c0f8852b12896461956324c669720714ecf2c3141e5084d33182d52bcbe0cff",
+ "size": 804973,
+ "filename": "vocab.lten.spm",
+ "location": "main-workspace/translations-models/412f7ecf-aa11-41dc-a9a2-3540213ea062.spm",
+ "mimetype": "text/plain"
+ },
+ "filter_expression": "env.channel == 'default' || env.channel == 'nightly'",
+ "id": "e6f0b29c-a795-4496-a847-e2029b3afa1c",
+ "last_modified": 1712068183356
+ },
+ {
"name": "model.enca.intgemm.alphas.bin",
"schema": 1710172593713,
"toLang": "ca",
@@ -2719,5 +2827,5 @@
"last_modified": 1701186751412
}
],
- "timestamp": 1710173317976
+ "timestamp": 1712849848180
}
diff --git a/services/settings/dumps/security-state/intermediates.json b/services/settings/dumps/security-state/intermediates.json
index 752d8fb270..ac343a2494 100644
--- a/services/settings/dumps/security-state/intermediates.json
+++ b/services/settings/dumps/security-state/intermediates.json
@@ -1,6 +1,222 @@
{
"data": [
{
+ "schema": 1712310550077,
+ "derHash": "w+vOp+axOsPrOnnwWBnmfWiTxlZC9Q2bXhZHyyZQa44=",
+ "subject": "CN=Security Communication ECC RootCA1,O=SECOM Trust Systems CO.\\,LTD.,C=JP",
+ "subjectDN": "MGExCzAJBgNVBAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0Ex",
+ "whitelist": false,
+ "attachment": {
+ "hash": "49bb804fc27ee287203f35432fa03118cbda037f2cc4b15a762a27278f2a430a",
+ "size": 1455,
+ "filename": "Mym_oTtgB6tfw3E_CssolCbi-8mcxcEQqRSxOVcWALY=.pem",
+ "location": "security-state-staging/intermediates/85f47822-4d0f-454d-9d84-f0ce5fc4d157.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "Mym/oTtgB6tfw3E/CssolCbi+8mcxcEQqRSxOVcWALY=",
+ "crlite_enrolled": false,
+ "id": "cdfe2baf-ce8e-4ff0-a638-e40128c6f4ee",
+ "last_modified": 1712311023721
+ },
+ {
+ "schema": 1712310549735,
+ "derHash": "nCgDadBy+J2cm2JkC78vKNLzDs7pdZhUAx1cXFPo0bY=",
+ "subject": "CN=JPRS OV ECC CA 2024 G1,O=Japan Registry Services Co.\\, Ltd.,C=JP",
+ "subjectDN": "MFoxCzAJBgNVBAYTAkpQMSowKAYDVQQKEyFKYXBhbiBSZWdpc3RyeSBTZXJ2aWNlcyBDby4sIEx0ZC4xHzAdBgNVBAMTFkpQUlMgT1YgRUNDIENBIDIwMjQgRzE=",
+ "whitelist": false,
+ "attachment": {
+ "hash": "8f21756b90356734138db1717baa7cd355f39bc01d0bbefcd782b6a73bb40267",
+ "size": 1284,
+ "filename": "v_cVY7f6bXpCh2OYwK6wmEE9ZIaufUJmoSlpxWvjBDI=.pem",
+ "location": "security-state-staging/intermediates/5d677b3c-4524-4ee7-a2b9-388e7d90dc2c.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "v/cVY7f6bXpCh2OYwK6wmEE9ZIaufUJmoSlpxWvjBDI=",
+ "crlite_enrolled": false,
+ "id": "f9ed868e-cbce-49b9-85ff-1b7d8f9771e7",
+ "last_modified": 1712311023718
+ },
+ {
+ "schema": 1712310549107,
+ "derHash": "G5AdVQpaUfZzcnnGAhRF8FG9KTAVYSpbqj+FiIKe5po=",
+ "subject": "CN=JPRS DV ECC CA 2024 G1,O=Japan Registry Services Co.\\, Ltd.,C=JP",
+ "subjectDN": "MFoxCzAJBgNVBAYTAkpQMSowKAYDVQQKEyFKYXBhbiBSZWdpc3RyeSBTZXJ2aWNlcyBDby4sIEx0ZC4xHzAdBgNVBAMTFkpQUlMgRFYgRUNDIENBIDIwMjQgRzE=",
+ "whitelist": false,
+ "attachment": {
+ "hash": "3749489691bfad9b5833e7e64c622b11fa2a70217f4d99f59629a2e6e864a189",
+ "size": 1284,
+ "filename": "d7IsXA0lsOv-SgxZRrDbM-A_cNQoAtC0iZ2kRR58b5c=.pem",
+ "location": "security-state-staging/intermediates/57cb9705-521a-4177-82fa-7c562bc5aef7.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "d7IsXA0lsOv+SgxZRrDbM+A/cNQoAtC0iZ2kRR58b5c=",
+ "crlite_enrolled": false,
+ "id": "cc33f7ee-ac11-426c-9210-dd79e9bde3ae",
+ "last_modified": 1712311023716
+ },
+ {
+ "schema": 1711724029637,
+ "derHash": "TkLi2IxOTbWRnHkATRycDpwtb7tFPaHWQtPTD3jAVy4=",
+ "subject": "CN=Certigna Server Authentication ACME FR CA G2,O=Certigna,C=FR",
+ "subjectDN": "MHYxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxNTAzBgNVBAMMLENlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIEZSIENBIEcy",
+ "whitelist": false,
+ "attachment": {
+ "hash": "47f464e31a2f05a550e83d8045d8570fcf9f57caeb7722436101c8940e879ca3",
+ "size": 2467,
+ "filename": "ULy4kAY07boZvLRDouxffHNVcsGHGQFCnEuCkq-cMCQ=.pem",
+ "location": "security-state-staging/intermediates/c4de029c-0d39-43ba-881d-1f1bdac8eff3.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "ULy4kAY07boZvLRDouxffHNVcsGHGQFCnEuCkq+cMCQ=",
+ "crlite_enrolled": false,
+ "id": "7d46e740-ae39-4f00-87ee-9a6e358e481d",
+ "last_modified": 1711724223131
+ },
+ {
+ "schema": 1711724029289,
+ "derHash": "yh/1TgMRhvkAa7FI+M5qU/BXQqS2zo/+ed/SrkceuqU=",
+ "subject": "CN=Certigna Server Authentication ACME FR CA G1,O=Certigna,C=FR",
+ "subjectDN": "MHYxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxNTAzBgNVBAMMLENlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIEZSIENBIEcx",
+ "whitelist": false,
+ "attachment": {
+ "hash": "661792dc63ededccb7dc2dfcd2918e5d8236c2dedcce5f8da637be0e28481a61",
+ "size": 2052,
+ "filename": "OQzKIkNRq8kQgHkelHOGFIGYVop2_zdqAGChunarhRQ=.pem",
+ "location": "security-state-staging/intermediates/728c8635-f40a-4594-87cd-5740ebcc5aab.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "OQzKIkNRq8kQgHkelHOGFIGYVop2/zdqAGChunarhRQ=",
+ "crlite_enrolled": false,
+ "id": "f14d65ec-1130-46a3-b84f-246c756cb63b",
+ "last_modified": 1711724223128
+ },
+ {
+ "schema": 1711724028964,
+ "derHash": "scjkcMofSIXd3uD+gLgF8sqCN3DsYHHBzOt39woPtsI=",
+ "subject": "CN=Certigna Server Authentication ACME CA G1,O=Certigna,C=FR",
+ "subjectDN": "MHMxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxMjAwBgNVBAMMKUNlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIENBIEcx",
+ "whitelist": false,
+ "attachment": {
+ "hash": "6808866c9da6c164a23ade7632d0cce4a43b5740def835d4020636b892aa342f",
+ "size": 2048,
+ "filename": "rvK65wj_zEVuddzfsGOjD135Wh2MILoAbXu7kKxNwoM=.pem",
+ "location": "security-state-staging/intermediates/e4364543-7c84-43b9-a09d-e59d2930b628.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "rvK65wj/zEVuddzfsGOjD135Wh2MILoAbXu7kKxNwoM=",
+ "crlite_enrolled": false,
+ "id": "8046050c-57cc-44c2-8341-e1801f5dde99",
+ "last_modified": 1711724223126
+ },
+ {
+ "schema": 1711724028612,
+ "derHash": "YP0d8Xerjt6HRy6OcoMH9W7EGmDMrXlMT9ttHLfUDVY=",
+ "subject": "CN=Certigna Server Authentication ACME CA G2,O=Certigna,C=FR",
+ "subjectDN": "MHMxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxMjAwBgNVBAMMKUNlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIENBIEcy",
+ "whitelist": false,
+ "attachment": {
+ "hash": "eac6be7c8bce492dda5b1ff24dea8088d2aacfea367f4d96fd3900d742aebb65",
+ "size": 2463,
+ "filename": "RKrRuFywubwqUKSLQ4_OujB1TYcPLRKs1GDCg351ako=.pem",
+ "location": "security-state-staging/intermediates/05a73290-0d3d-49c1-840a-22d2de8c1fb6.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "RKrRuFywubwqUKSLQ4/OujB1TYcPLRKs1GDCg351ako=",
+ "crlite_enrolled": false,
+ "id": "2269c12e-3b8e-4700-a4d5-42525a948a50",
+ "last_modified": 1711724223123
+ },
+ {
+ "schema": 1711464837732,
+ "derHash": "8H04OCfiZDJsXQ2+JXqoIQhAnyUkb4HuNsivIitaHkM=",
+ "subject": "CN=Telia RSA OV CA v4,O=Telia Company AB,C=SE",
+ "subjectDN": "MEUxCzAJBgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMRswGQYDVQQDDBJUZWxpYSBSU0EgT1YgQ0EgdjQ=",
+ "whitelist": false,
+ "attachment": {
+ "hash": "8a8a355227c0c3345470ef0bfb850234b21b1dd60470e4dcf566cfdff0e502f8",
+ "size": 2300,
+ "filename": "uNTFkYMpAkhXFadzPws2J6iLVX44ue38H6LKC6alh3A=.pem",
+ "location": "security-state-staging/intermediates/74ec359d-1abe-4c34-9800-83d22cf1824f.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "uNTFkYMpAkhXFadzPws2J6iLVX44ue38H6LKC6alh3A=",
+ "crlite_enrolled": false,
+ "id": "2b785473-c7ec-4117-be0b-1f5cbe2c9bca",
+ "last_modified": 1711465023926
+ },
+ {
+ "schema": 1711464836863,
+ "derHash": "pn8+Z8sd8urPOkBOr/F5N05eP0ug2C6RQF2urfsPM30=",
+ "subject": "CN=Telia RSA DV CA v4,O=Telia Company AB,C=SE",
+ "subjectDN": "MEUxCzAJBgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMRswGQYDVQQDDBJUZWxpYSBSU0EgRFYgQ0EgdjQ=",
+ "whitelist": false,
+ "attachment": {
+ "hash": "dd2acaf8dd832742c4ce4c63de0d80621f24a60707462a487a496d4fd1271456",
+ "size": 2300,
+ "filename": "pbnDROwxL9imiYDtQcKHL4D07MvFyHJGsfaj_hTH-uw=.pem",
+ "location": "security-state-staging/intermediates/7acb6902-fa54-421e-9b3f-bbb075b0e656.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "pbnDROwxL9imiYDtQcKHL4D07MvFyHJGsfaj/hTH+uw=",
+ "crlite_enrolled": false,
+ "id": "7e90cfb9-3e37-47af-898a-e5d61b99b444",
+ "last_modified": 1711465023924
+ },
+ {
+ "schema": 1711464837319,
+ "derHash": "JtdCdBukKDlPkE5EEO1tqS9NDFVSHx7eKyl5f4ZslVw=",
+ "subject": "CN=Telia EC DV CA v4,O=Telia Company AB,C=SE",
+ "subjectDN": "MEQxCzAJBgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMRowGAYDVQQDDBFUZWxpYSBFQyBEViBDQSB2NA==",
+ "whitelist": false,
+ "attachment": {
+ "hash": "c085e851d30509db40952fc9f84105396c73546c29ad715cda6452e1c7e04e1d",
+ "size": 1150,
+ "filename": "NeN7Ibyh_EFluoZE27OfNSDLpVsqzCOtSIe0YDz2GSA=.pem",
+ "location": "security-state-staging/intermediates/81c10429-00de-407d-a5c2-78f8d1a46d0d.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "NeN7Ibyh/EFluoZE27OfNSDLpVsqzCOtSIe0YDz2GSA=",
+ "crlite_enrolled": false,
+ "id": "ccbb9d09-e78a-42eb-84e3-ec36872e58a4",
+ "last_modified": 1711465023921
+ },
+ {
+ "schema": 1711079327797,
+ "derHash": "GnD+GCdR5jfA5dPVU+db26HWVf/lQR2bOQtzyDePpyw=",
+ "subject": "CN=Leocert TLS Issuing RSA CA 1,O=Leocert LLC,C=US",
+ "subjectDN": "MEoxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtMZW9jZXJ0IExMQzElMCMGA1UEAwwcTGVvY2VydCBUTFMgSXNzdWluZyBSU0EgQ0EgMQ==",
+ "whitelist": false,
+ "attachment": {
+ "hash": "909b2e8999e89c9ba6a76900402d0f011877b2cac32792910aa1f10c33ab823b",
+ "size": 2064,
+ "filename": "TLG3k1viplwCa4FIN66yKHXwUwpjuk-zYaXHlJKI1mY=.pem",
+ "location": "security-state-staging/intermediates/eafb2b1c-e818-4c8e-914b-86ac8c5d7235.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "TLG3k1viplwCa4FIN66yKHXwUwpjuk+zYaXHlJKI1mY=",
+ "crlite_enrolled": false,
+ "id": "b1c85617-4d30-44d3-ab20-4d5ea7d6ac51",
+ "last_modified": 1711079823132
+ },
+ {
+ "schema": 1711079328231,
+ "derHash": "eQidoGeOCFCA5Y1UxGKzE+94QWqHJPrOfRqJN0OOAMc=",
+ "subject": "CN=Leocert TLS Issuing ECC CA 1,O=Leocert LLC,C=US",
+ "subjectDN": "MEoxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtMZW9jZXJ0IExMQzElMCMGA1UEAwwcTGVvY2VydCBUTFMgSXNzdWluZyBFQ0MgQ0EgMQ==",
+ "whitelist": false,
+ "attachment": {
+ "hash": "3c2391cb93c7441025a1e056e161c4d31acb51039c0f8720ee0598ebe26e35d6",
+ "size": 1089,
+ "filename": "VCKMSEPlE_Wm1yh1cVkPT9qFW95J646OM9DdihnTErc=.pem",
+ "location": "security-state-staging/intermediates/7d525c97-e82d-4f3a-be44-21c246617a24.pem",
+ "mimetype": "application/x-pem-file"
+ },
+ "pubKeyHash": "VCKMSEPlE/Wm1yh1cVkPT9qFW95J646OM9DdihnTErc=",
+ "crlite_enrolled": false,
+ "id": "2945ac79-f854-4685-a0ec-f140cdf5e8c7",
+ "last_modified": 1711079823129
+ },
+ {
"schema": 1710946446035,
"derHash": "8DZVz+hbOj8q8v03yrnqKDs6KKm2FX/pvrdcF8BhGzo=",
"subject": "CN=Keysec GR3 DV TLS CA 2024,O=KEYSEC LTDA,C=BR",
@@ -415,78 +631,6 @@
"last_modified": 1710557823219
},
{
- "schema": 1710539501295,
- "derHash": "kPqg3vqxPQmsAJdRwMO2BqR81X3oAi5od0gZm0RHSyM=",
- "subject": "CN=Certigna Server Authentication ACME CA,O=Certigna,C=FR",
- "subjectDN": "MHAxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxLzAtBgNVBAMMJkNlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIENB",
- "whitelist": false,
- "attachment": {
- "hash": "49c8ef6d8a7b25da0bf8610a646bed6d9d0eac47d63eec405c40c5e71ce8208f",
- "size": 2458,
- "filename": "g96_QpC6c95hPe0mJuHADgpPz6RuVaqjHoRSjOyuL7E=.pem",
- "location": "security-state-staging/intermediates/7b7f68ae-bfd3-4840-b207-9ab77f4232f8.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "g96/QpC6c95hPe0mJuHADgpPz6RuVaqjHoRSjOyuL7E=",
- "crlite_enrolled": false,
- "id": "38d23199-bfbc-46eb-bc42-70a370fbac24",
- "last_modified": 1710539831556
- },
- {
- "schema": 1710539500206,
- "derHash": "WMKULEG5CPAJ6QD+YtETC76npaiDbyCJkImnqVHGQyc=",
- "subject": "CN=Certigna Server Authentication ACME FR CA 2024,O=Certigna,C=FR",
- "subjectDN": "MHgxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxNzA1BgNVBAMMLkNlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIEZSIENBIDIwMjQ=",
- "whitelist": false,
- "attachment": {
- "hash": "b2210ca67012f7e0ba1fde2dc3546563fa2c16b17753d83a23cf28772fdc78d4",
- "size": 2052,
- "filename": "DoTA9i3cZ3sCoyIkBYv0KN9rxjve3-F1LOL0hMTEjp8=.pem",
- "location": "security-state-staging/intermediates/bcda003c-3561-469c-97ec-e1268e4edb50.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "DoTA9i3cZ3sCoyIkBYv0KN9rxjve3+F1LOL0hMTEjp8=",
- "crlite_enrolled": false,
- "id": "506528ab-12d0-4986-8355-f72a795f5311",
- "last_modified": 1710539831553
- },
- {
- "schema": 1710539500937,
- "derHash": "4FD1W1Vj5DXdNgI6OjtKy+q0TDlk8YBIecwxEeKH2Bs=",
- "subject": "CN=Certigna Server Authentication ACME FR CA,O=Certigna,C=FR",
- "subjectDN": "MHMxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxMjAwBgNVBAMMKUNlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIEZSIENB",
- "whitelist": false,
- "attachment": {
- "hash": "bbe52c415c1b8f00ea91967db06b87b829f88f35160ca7e8ff01d6b9acaa0e63",
- "size": 2463,
- "filename": "zkN9UXAo8i_VmU2mjpCUOtef-5jkt-KPl32fIqB0tNE=.pem",
- "location": "security-state-staging/intermediates/4c8342ae-ea33-4964-bac7-4ef7d2d40537.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "zkN9UXAo8i/VmU2mjpCUOtef+5jkt+KPl32fIqB0tNE=",
- "crlite_enrolled": false,
- "id": "14b7e624-637a-4380-95e3-3b432fc6aac2",
- "last_modified": 1710539831550
- },
- {
- "schema": 1710539500588,
- "derHash": "qaY+A/DUmOxyQwJqeo8r5wXQTyhF1J9j2InJ3QtKOUQ=",
- "subject": "CN=Certigna Server Authentication ACME CA 2024,O=Certigna,C=FR",
- "subjectDN": "MHUxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0aWduYTEdMBsGA1UEYQwUTlRSRlItNDgxNDYzMDgxMDAwMzYxNDAyBgNVBAMMK0NlcnRpZ25hIFNlcnZlciBBdXRoZW50aWNhdGlvbiBBQ01FIENBIDIwMjQ=",
- "whitelist": false,
- "attachment": {
- "hash": "161dc34976ab4cfc42bd69ec0b38ae28178722e6243ee46a916e76743d135d0e",
- "size": 2048,
- "filename": "5t6bUEJAtYeRqDL4wc0-BLSX3Cy-W80bNcuO4zpx08Q=.pem",
- "location": "security-state-staging/intermediates/fce8c500-791e-481c-8225-5faf94b2f6e3.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "5t6bUEJAtYeRqDL4wc0+BLSX3Cy+W80bNcuO4zpx08Q=",
- "crlite_enrolled": false,
- "id": "08355b95-d61d-4aa6-8185-4c6a16379fae",
- "last_modified": 1710539831545
- },
- {
"schema": 1709780043992,
"derHash": "6CGxESkRBWfxYH8LDzaOx42BFBjwxIzEJUUPY+uICV0=",
"subject": "CN=Entrust 4K EV TLS Root CA - 2022,O=Entrust\\, Inc.,C=US",
@@ -12817,24 +12961,6 @@
"last_modified": 1666727870028
},
{
- "schema": 1666727330223,
- "derHash": "0zqEf2QDd74K4aQpFToH6HyIJ/pIQLUVi83LheEKRTo=",
- "subject": "CN=VR IDENT EV SSL CA 2020,O=Fiducia & GAD IT AG,C=DE",
- "subjectDN": "ME0xCzAJBgNVBAYTAkRFMRwwGgYDVQQKDBNGaWR1Y2lhICYgR0FEIElUIEFHMSAwHgYDVQQDDBdWUiBJREVOVCBFViBTU0wgQ0EgMjAyMA==",
- "whitelist": false,
- "attachment": {
- "hash": "a95e7ccf2ef4b020f65fbd061c44e49d966821be27623376e3f16a8a3ebd371f",
- "size": 2361,
- "filename": "te09bIALahNazDua57TEibeM7CatIkOAT8t-qu_Kdro=.pem",
- "location": "security-state-staging/intermediates/e8b0b0a2-f7f0-4893-b9ef-32f493e8a146.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "te09bIALahNazDua57TEibeM7CatIkOAT8t+qu/Kdro=",
- "crlite_enrolled": false,
- "id": "92b7ea6b-c8ed-47fc-bd89-4834ef4a3ad5",
- "last_modified": 1666727870010
- },
- {
"schema": 1666727401140,
"derHash": "y2Zmsyv/Lv7cxBh98Umm00pdELcWW5z/KmfA4xGu7tc=",
"subject": "CN=QuoVadis Europe EV SSL CA G1,O=QuoVadis Trustlink B.V.,C=NL",
@@ -12889,24 +13015,6 @@
"last_modified": 1666727869969
},
{
- "schema": 1666727372418,
- "derHash": "NH0Y3Mwu/FGpIOen+7B7+9o1YTaB+C3KXExyuwyDwDU=",
- "subject": "CN=VR IDENT SSL CA 2020,O=Fiducia & GAD IT AG,C=DE",
- "subjectDN": "MEoxCzAJBgNVBAYTAkRFMRwwGgYDVQQKDBNGaWR1Y2lhICYgR0FEIElUIEFHMR0wGwYDVQQDDBRWUiBJREVOVCBTU0wgQ0EgMjAyMA==",
- "whitelist": false,
- "attachment": {
- "hash": "4158e246be129509cb8d8eb14b647517fea0196e81a9d593f4b8f64a11a6414f",
- "size": 2357,
- "filename": "fQUctsUYoew1aFnAFT0prf-kJmkVNqkPo2jJ5Jm9RaA=.pem",
- "location": "security-state-staging/intermediates/ada7bb67-cb15-43b5-9c74-259e711941b5.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "fQUctsUYoew1aFnAFT0prf+kJmkVNqkPo2jJ5Jm9RaA=",
- "crlite_enrolled": false,
- "id": "2b1f7c04-dd89-4f96-ad7d-e4e167a9099d",
- "last_modified": 1666727869953
- },
- {
"schema": 1666727415303,
"derHash": "n/I8uTh7ngCDvVqhlU7t33kokKqOZ81NON0or0pDmtg=",
"subject": "CN=AC SERVIDORES SEGUROS TIPO2,OU=Ceres,O=FNMT-RCM,C=ES",
@@ -13627,24 +13735,6 @@
"last_modified": 1666727869297
},
{
- "schema": 1666727410331,
- "derHash": "6uWboswzKBpc3exQz7/Z3QylVl8872mMk2ahDOwIf5c=",
- "subject": "CN=Domain The Net Technologies Ltd CA for SSL R2,O=Domain The Net Technologies Ltd,C=IL",
- "subjectDN": "MG8xCzAJBgNVBAYTAklMMSgwJgYDVQQKDB9Eb21haW4gVGhlIE5ldCBUZWNobm9sb2dpZXMgTHRkMTYwNAYDVQQDDC1Eb21haW4gVGhlIE5ldCBUZWNobm9sb2dpZXMgTHRkIENBIGZvciBTU0wgUjI=",
- "whitelist": false,
- "attachment": {
- "hash": "74b27e430703697e4e0eb4a4518f35a8e27789b980b29dd2593702248ea4491d",
- "size": 2483,
- "filename": "1FBqLyRsP8ibxXXsW64LYWGTeYMGSsUTMFEetUQakD8=.pem",
- "location": "security-state-staging/intermediates/beb201df-0f86-438c-911c-f798428aa9c4.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "1FBqLyRsP8ibxXXsW64LYWGTeYMGSsUTMFEetUQakD8=",
- "crlite_enrolled": false,
- "id": "ac504f95-1d16-427c-8e82-82da28c34bc6",
- "last_modified": 1666727869284
- },
- {
"schema": 1666727422380,
"derHash": "UpmYc6PRKkVx25oWBXap2VGtTkp9oxxcEGE4BekiQyU=",
"subject": "CN=MuaSSL.com TLS Issuing RSA CA R1,O=Hao Quang Viet Software Company Limited,C=VN",
@@ -23707,24 +23797,6 @@
"last_modified": 1661561823060
},
{
- "schema": 1659747423666,
- "derHash": "PzHbdYKNqpbk3luCoGeP4gI71bJGw/klDL9nGOEJWPU=",
- "subject": "CN=Domain The Net Technologies Ltd CA for EV SSL R2,O=Domain The Net Technologies Ltd,C=IL",
- "subjectDN": "MHIxCzAJBgNVBAYTAklMMSgwJgYDVQQKDB9Eb21haW4gVGhlIE5ldCBUZWNobm9sb2dpZXMgTHRkMTkwNwYDVQQDDDBEb21haW4gVGhlIE5ldCBUZWNobm9sb2dpZXMgTHRkIENBIGZvciBFViBTU0wgUjI=",
- "whitelist": false,
- "attachment": {
- "hash": "8a4d28681845cdc8a071852db1380fdac3ad47d045153ee6fc939a47193b2bff",
- "size": 2487,
- "filename": "Lk19AkNIC7AwHNF5HsU_phCEnUBI-eiA0mbFhxxeQsQ=.pem",
- "location": "security-state-staging/intermediates/88af22d0-cfec-40f3-bb5e-5e0f70ca65bf.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "Lk19AkNIC7AwHNF5HsU/phCEnUBI+eiA0mbFhxxeQsQ=",
- "crlite_enrolled": false,
- "id": "ebf62e8f-f268-416f-b7de-23e67b4313c9",
- "last_modified": 1660093023424
- },
- {
"schema": 1659617319739,
"derHash": "r9ep9iQLEcBtD2ck1E9FTg1eMX1cz/a4GNVyRO7VoZk=",
"subject": "CN=Positiwise OV SSL CA,O=Positiwise Software LLC,C=US",
@@ -27433,24 +27505,6 @@
"last_modified": 1618102648566
},
{
- "schema": 1616744987712,
- "derHash": "Grxa1bw5EaW0qR5Cu6MhLiqj3IAUfv4dSVcuS+BTJjM=",
- "subject": "CN=GlobalSign Atlas R3 AlphaSSL CA H1 2021,O=Globalsign nv-sa,C=BE",
- "subjectDN": "MFoxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxzaWduIG52LXNhMTAwLgYDVQQDEydHbG9iYWxTaWduIEF0bGFzIFIzIEFscGhhU1NMIENBIEgxIDIwMjE=",
- "whitelist": false,
- "attachment": {
- "hash": "0ac4293f335c1f7922726fccc78a0a749002116e0d6d354411046b3681c14fe0",
- "size": 1715,
- "filename": "HAdiOrIGPG14XkymnmeQ184Cm1Z8E9-jnOoBut3PORw=.pem",
- "location": "security-state-staging/intermediates/a79140b4-b597-4021-b12a-e83c7b03a7e0.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "HAdiOrIGPG14XkymnmeQ184Cm1Z8E9+jnOoBut3PORw=",
- "crlite_enrolled": false,
- "id": "afbdb701-e59b-4398-967c-b0db2394d003",
- "last_modified": 1616745549953
- },
- {
"schema": 1615384231447,
"derHash": "sQhhc3s+ybEfphVNI5cP+y2r/Ciuamv1bD8yBCZiOa0=",
"subject": "CN=SECOM Passport for Member PUB CA4,OU=SECOM Passport for Member 2.0 PUB,O=SECOM Trust Systems CO.\\,LTD.,C=JP",
@@ -27667,24 +27721,6 @@
"last_modified": 1601517444025
},
{
- "schema": 1601376747224,
- "derHash": "uQ7q6THl4rfTNfFJ2mwiEJhgANIU/9tipy9zMtY3Ma8=",
- "subject": "CN=Verizon Global Root CA,OU=OmniRoot,O=Verizon Business,C=US",
- "subjectDN": "MFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBWZXJpem9uIEJ1c2luZXNzMREwDwYDVQQLEwhPbW5pUm9vdDEfMB0GA1UEAxMWVmVyaXpvbiBHbG9iYWwgUm9vdCBDQQ==",
- "whitelist": false,
- "attachment": {
- "hash": "b2ff06177ee90557f82e4d2b65f87cbe3e37ef185fd9f5b2f7d8c654bf7eefbc",
- "size": 1760,
- "filename": "v-gpCYcuRDTxFcUaVhaAGVlNDgPco2PZ87SDnQurzeU=.pem",
- "location": "security-state-staging/intermediates/96d8b2a4-5c83-408a-aba1-f8c0818c74a1.pem",
- "mimetype": "application/x-pem-file"
- },
- "pubKeyHash": "v+gpCYcuRDTxFcUaVhaAGVlNDgPco2PZ87SDnQurzeU=",
- "crlite_enrolled": false,
- "id": "90eff404-0514-42a4-b6bc-375d060840d8",
- "last_modified": 1601517443892
- },
- {
"schema": 1601376768762,
"derHash": "bay7iUUTex2tQhGwQ2774G8SrONpBJc7Ra4ldAgj02k=",
"subject": "CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US",
@@ -30565,5 +30601,5 @@
"last_modified": 1559865884636
}
],
- "timestamp": 1710946623378
+ "timestamp": 1712674623155
}
diff --git a/services/settings/test/unit/test_attachments_downloader.js b/services/settings/test/unit/test_attachments_downloader.js
index 86dd52b729..284294cfde 100644
--- a/services/settings/test/unit/test_attachments_downloader.js
+++ b/services/settings/test/unit/test_attachments_downloader.js
@@ -47,7 +47,7 @@ function pathFromURL(url) {
const PROFILE_URL = PathUtils.toFileURI(PathUtils.localProfileDir);
-function run_test() {
+add_setup(() => {
server = new HttpServer();
server.start(-1);
registerCleanupFunction(() => server.stop(() => {}));
@@ -56,9 +56,7 @@ function run_test() {
"/cdn/main-workspace/some-collection/",
do_get_file("test_attachments_downloader")
);
-
- run_next_test();
-}
+});
async function clear_state() {
Services.prefs.setStringPref(
@@ -68,9 +66,9 @@ async function clear_state() {
downloader = new Downloader("main", "some-collection");
const dummyCacheImpl = {
- get: async attachmentId => {},
- set: async (attachmentId, attachment) => {},
- delete: async attachmentId => {},
+ get: async () => {},
+ set: async () => {},
+ delete: async () => {},
};
// The download() method requires a cacheImpl, but the Downloader
// class does not have one. Define a dummy no-op one.
@@ -388,7 +386,7 @@ async function doTestDownloadCacheImpl({ simulateCorruption }) {
throw new Error("Simulation of corrupted cache (write)");
}
},
- async delete(attachmentId) {},
+ async delete() {},
};
Object.defineProperty(downloader, "cacheImpl", { value: cacheImpl });
@@ -621,6 +619,74 @@ add_task(async function test_download_from_dump() {
// but added for consistency with other tests tasks around here.
add_task(clear_state);
+add_task(async function test_attachment_get() {
+ // Since get() is largely a wrapper around the same code as download(),
+ // we only test a couple of parts to check it functions as expected, and
+ // rely on the download() testing for the rest.
+
+ await Assert.rejects(
+ downloader.get(RECORD),
+ /NotFoundError: Could not find /,
+ "get() fails when there is no local cache nor dump"
+ );
+
+ const client = RemoteSettings("dump-collection", {
+ bucketName: "dump-bucket",
+ });
+
+ // Temporarily replace the resource:-URL with another resource:-URL.
+ const orig_RESOURCE_BASE_URL = Downloader._RESOURCE_BASE_URL;
+ Downloader._RESOURCE_BASE_URL = "resource://rs-downloader-test";
+ const resProto = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProto.setSubstitution(
+ "rs-downloader-test",
+ Services.io.newFileURI(do_get_file("test_attachments_downloader"))
+ );
+
+ function checkInfo(result, expectedSource, expectedRecord = RECORD_OF_DUMP) {
+ Assert.equal(
+ new TextDecoder().decode(new Uint8Array(result.buffer)),
+ "This would be a RS dump.\n",
+ "expected content from dump"
+ );
+ Assert.deepEqual(result.record, expectedRecord, "expected record for dump");
+ Assert.equal(result._source, expectedSource, "expected source of dump");
+ }
+
+ // When a record is given, use whichever that has the matching last_modified.
+ const dump = await client.attachments.get(RECORD_OF_DUMP);
+ checkInfo(dump, "dump_match");
+
+ await client.attachments.deleteDownloaded(RECORD_OF_DUMP);
+
+ await Assert.rejects(
+ client.attachments.get(null, {
+ attachmentId: "filename-without-meta.txt",
+ fallbackToDump: true,
+ }),
+ /NotFoundError: Could not find filename-without-meta.txt in cache or dump/,
+ "Cannot download dump that lacks a .meta.json file"
+ );
+
+ await Assert.rejects(
+ client.attachments.get(null, {
+ attachmentId: "filename-without-content.txt",
+ fallbackToDump: true,
+ }),
+ /Could not download resource:\/\/rs-downloader-test\/settings\/dump-bucket\/dump-collection\/filename-without-content\.txt(?!\.meta\.json)/,
+ "Cannot download dump that is missing, despite the existing .meta.json"
+ );
+
+ // Restore, just in case.
+ Downloader._RESOURCE_BASE_URL = orig_RESOURCE_BASE_URL;
+ resProto.setSubstitution("rs-downloader-test", null);
+});
+// Not really needed because the last test doesn't modify the main collection,
+// but added for consistency with other tests tasks around here.
+add_task(clear_state);
+
add_task(async function test_obsolete_attachments_are_pruned() {
const RECORD2 = {
...RECORD,
diff --git a/services/settings/test/unit/test_remote_settings.js b/services/settings/test/unit/test_remote_settings.js
index 0937acb519..382d1aa983 100644
--- a/services/settings/test/unit/test_remote_settings.js
+++ b/services/settings/test/unit/test_remote_settings.js
@@ -133,7 +133,11 @@ add_task(
await clientWithDump.maybeSync(timestamp);
const list = await clientWithDump.get();
- ok(list.length > 20, `The dump was loaded (${list.length} records)`);
+ Assert.greater(
+ list.length,
+ 20,
+ `The dump was loaded (${list.length} records)`
+ );
equal(received.created[0].id, "xx", "Record from the sync come first.");
const createdById = received.created.reduce((acc, r) => {
@@ -328,8 +332,9 @@ add_task(async function test_get_sorts_results_if_specified() {
);
const records = await client.get({ order: "field" });
- ok(
- records[0].field < records[records.length - 1].field,
+ Assert.less(
+ records[0].field,
+ records[records.length - 1].field,
"records are sorted"
);
});
@@ -350,6 +355,7 @@ add_task(async function test_get_falls_back_sorts_results() {
order: "-id",
});
+ // eslint-disable-next-line mozilla/no-comparison-or-assignment-inside-ok
ok(records[0].id > records[records.length - 1].id, "records are sorted");
clientWithDump.db.getLastModified = backup;
@@ -539,7 +545,7 @@ add_task(async function test_get_does_not_verify_signature_if_load_dump() {
let called;
clientWithDump._verifier = {
- async asyncVerifyContentSignature(serialized, signature) {
+ async asyncVerifyContentSignature() {
called = true;
return true;
},
@@ -577,7 +583,7 @@ add_task(
const backup = clientWithDump._verifier;
let callCount = 0;
clientWithDump._verifier = {
- async asyncVerifyContentSignature(serialized, signature) {
+ async asyncVerifyContentSignature() {
callCount++;
return true;
},
@@ -634,7 +640,7 @@ add_task(
let called;
clientWithDump._verifier = {
- async asyncVerifyContentSignature(serialized, signature) {
+ async asyncVerifyContentSignature() {
called = true;
return true;
},
@@ -1168,7 +1174,7 @@ add_task(clear_state);
add_task(async function test_sync_event_is_not_sent_from_get_when_no_dump() {
let called = false;
- client.on("sync", e => {
+ client.on("sync", () => {
called = true;
});
diff --git a/services/settings/test/unit/test_remote_settings_dump_lastmodified.js b/services/settings/test/unit/test_remote_settings_dump_lastmodified.js
index 1cce089ff7..25de34c1be 100644
--- a/services/settings/test/unit/test_remote_settings_dump_lastmodified.js
+++ b/services/settings/test/unit/test_remote_settings_dump_lastmodified.js
@@ -14,7 +14,11 @@ async function getLocalDumpLastModified(bucket, collection) {
return -1;
}
const { timestamp } = await res.json();
- ok(timestamp >= 0, `${bucket}/${collection} dump has timestamp`);
+ Assert.greaterOrEqual(
+ timestamp,
+ 0,
+ `${bucket}/${collection} dump has timestamp`
+ );
return timestamp;
}
@@ -51,5 +55,5 @@ add_task(async function lastModified_summary_is_correct() {
equal(lastModified, actual, `last_modified should match collection`);
checked++;
}
- ok(checked > 0, "At least one dump was packaged and checked.");
+ Assert.greater(checked, 0, "At least one dump was packaged and checked.");
});
diff --git a/services/settings/test/unit/test_remote_settings_offline.js b/services/settings/test/unit/test_remote_settings_offline.js
index ffb810829d..0a250c3e0a 100644
--- a/services/settings/test/unit/test_remote_settings_offline.js
+++ b/services/settings/test/unit/test_remote_settings_offline.js
@@ -107,7 +107,11 @@ add_task(clear_state);
add_task(async function test_load_dump_after_non_empty_import() {
// Dump is updated regularly, verify that the dump matches our expectations
// before running the test.
- ok(DUMP_LAST_MODIFIED > 1234, "Assuming dump to be newer than dummy 1234");
+ Assert.greater(
+ DUMP_LAST_MODIFIED,
+ 1234,
+ "Assuming dump to be newer than dummy 1234"
+ );
await importData([{ last_modified: 1234, id: "dummy" }]);
@@ -120,7 +124,11 @@ add_task(clear_state);
add_task(async function test_load_dump_after_import_from_broken_distro() {
// Dump is updated regularly, verify that the dump matches our expectations
// before running the test.
- ok(DUMP_LAST_MODIFIED > 1234, "Assuming dump to be newer than dummy 1234");
+ Assert.greater(
+ DUMP_LAST_MODIFIED,
+ 1234,
+ "Assuming dump to be newer than dummy 1234"
+ );
// No last_modified time.
await importData([{ id: "dummy" }]);
diff --git a/services/settings/test/unit/test_remote_settings_poll.js b/services/settings/test/unit/test_remote_settings_poll.js
index 3bf389ea34..c8025f4b7b 100644
--- a/services/settings/test/unit/test_remote_settings_poll.js
+++ b/services/settings/test/unit/test_remote_settings_poll.js
@@ -188,7 +188,7 @@ add_task(async function test_check_success() {
// Ensure that the remote-settings:changes-poll-end notification works
let notificationObserved = false;
const observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
notificationObserved = true;
},
@@ -258,7 +258,7 @@ add_task(async function test_update_timer_interface() {
await new Promise(resolve => {
const e = "remote-settings:changes-poll-end";
const changesPolledObserver = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(this, e);
resolve();
},
@@ -288,7 +288,7 @@ add_task(async function test_check_up_to_date() {
// Ensure that the remote-settings:changes-poll-end notification is sent.
let notificationObserved = false;
const observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
notificationObserved = true;
},
@@ -686,7 +686,7 @@ add_task(async function test_server_error() {
let notificationObserved = false;
const observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
notificationObserved = true;
},
@@ -807,7 +807,7 @@ add_task(async function test_client_error() {
let notificationsObserved = [];
const observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
Services.obs.removeObserver(this, aTopic);
notificationsObserved.push([aTopic, aSubject.wrappedJSObject]);
},
@@ -935,7 +935,7 @@ add_task(
// Wait for the "sync-broken-error" notification.
let notificationObserved = false;
const observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
notificationObserved = true;
},
};
diff --git a/services/settings/test/unit/test_remote_settings_signatures.js b/services/settings/test/unit/test_remote_settings_signatures.js
index 840cc24dd8..a730ba185e 100644
--- a/services/settings/test/unit/test_remote_settings_signatures.js
+++ b/services/settings/test/unit/test_remote_settings_signatures.js
@@ -487,7 +487,7 @@ add_task(async function test_check_synchronization_with_signatures() {
);
let syncEventSent = false;
- client.on("sync", ({ data }) => {
+ client.on("sync", () => {
syncEventSent = true;
});
@@ -542,7 +542,7 @@ add_task(async function test_check_synchronization_with_signatures() {
registerHandlers(badSigGoodOldResponses);
syncEventSent = false;
- client.on("sync", ({ data }) => {
+ client.on("sync", () => {
syncEventSent = true;
});
@@ -783,7 +783,7 @@ add_task(async function test_check_synchronization_with_signatures() {
const sigCalls = [];
let i = 0;
client._verifier = {
- async asyncVerifyContentSignature(serialized, signature) {
+ async asyncVerifyContentSignature(serialized) {
sigCalls.push(serialized);
console.log(`verify call ${i}`);
return [
diff --git a/services/settings/test/unit/test_remote_settings_worker.js b/services/settings/test/unit/test_remote_settings_worker.js
index 42b85bb92c..e2dcdb0063 100644
--- a/services/settings/test/unit/test_remote_settings_worker.js
+++ b/services/settings/test/unit/test_remote_settings_worker.js
@@ -82,8 +82,8 @@ add_task(async function test_throws_error_if_worker_fails_async() {
// should be reported to the caller.
await new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase("remote-settings");
- request.onsuccess = event => resolve();
- request.onblocked = event => reject(new Error("Cannot delete DB"));
+ request.onsuccess = () => resolve();
+ request.onblocked = () => reject(new Error("Cannot delete DB"));
request.onerror = event => reject(event.target.error);
});
let error;
diff --git a/services/settings/test/unit/test_shutdown_handling.js b/services/settings/test/unit/test_shutdown_handling.js
index a35ab6080a..2c98f0ab9b 100644
--- a/services/settings/test/unit/test_shutdown_handling.js
+++ b/services/settings/test/unit/test_shutdown_handling.js
@@ -41,7 +41,7 @@ add_task(async function test_shutdown_abort_after_start() {
const request = store
.index("cid")
.openCursor(IDBKeyRange.only("foopydoo/foo"));
- request.onsuccess = event => {
+ request.onsuccess = () => {
makeRequest();
};
}
@@ -74,7 +74,7 @@ add_task(async function test_shutdown_immediate_abort() {
let request = store
.index("cid")
.openCursor(IDBKeyRange.only("foopydoo/foo"));
- request.onsuccess = event => {
+ request.onsuccess = () => {
// Abort immediately.
Database._shutdownHandler();
request = store
diff --git a/services/sync/Weave.sys.mjs b/services/sync/Weave.sys.mjs
index 05a7031a73..1c4bb44928 100644
--- a/services/sync/Weave.sys.mjs
+++ b/services/sync/Weave.sys.mjs
@@ -156,7 +156,7 @@ AboutWeaveLog.prototype = {
"nsISupportsWeakReference",
]),
- getURIFlags(aURI) {
+ getURIFlags() {
return 0;
},
diff --git a/services/sync/docs/engines.rst b/services/sync/docs/engines.rst
index 7a4fa721af..a9a60d4e79 100644
--- a/services/sync/docs/engines.rst
+++ b/services/sync/docs/engines.rst
@@ -80,7 +80,7 @@ are open on other devices. There's no database - if we haven't synced yet we
don't know what other tabs are open, and when we do know, the list is just
stored in memory.
-The `SyncedTabs module <https://searchfox.org/mozilla-central/source/services/sync/modules/SyncedTabs.jsm>`_
+The `SyncedTabs module <https://searchfox.org/mozilla-central/source/services/sync/modules/SyncedTabs.sys.mjs>`_
is the main interface the browser uses to get the list of tabs from other
devices.
@@ -111,7 +111,7 @@ treat them as a single engine in practice.
As a result, only a shim is in the `services/sync/modules/engines/` directory,
while the actual logic is
-`next to the storage implementation <https://searchfox.org/mozilla-central/source/toolkit/components/formautofill/FormAutofillSync.jsm>`_.
+`next to the storage implementation <https://searchfox.org/mozilla-central/source/toolkit/components/formautofill/FormAutofillSync.sys.mjs>`_.
This engine has a unique twist on the "mirror" concept described above -
whenever a change is made to a fields, the original value of the field is
diff --git a/services/sync/modules-testing/fakeservices.sys.mjs b/services/sync/modules-testing/fakeservices.sys.mjs
index 4fd7534bf1..bf2d143203 100644
--- a/services/sync/modules-testing/fakeservices.sys.mjs
+++ b/services/sync/modules-testing/fakeservices.sys.mjs
@@ -29,7 +29,7 @@ export function FakeFilesystemService(contents) {
self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
};
- Utils.jsonLoad = async function jsonLoad(filePath, that) {
+ Utils.jsonLoad = async function jsonLoad(filePath) {
let obj;
let json = self.fakeContents["weave/" + filePath + ".json"];
if (json) {
@@ -38,14 +38,14 @@ export function FakeFilesystemService(contents) {
return obj;
};
- Utils.jsonMove = function jsonMove(aFrom, aTo, that) {
+ Utils.jsonMove = function jsonMove(aFrom, aTo) {
const fromPath = "weave/" + aFrom + ".json";
self.fakeContents["weave/" + aTo + ".json"] = self.fakeContents[fromPath];
delete self.fakeContents[fromPath];
return Promise.resolve();
};
- Utils.jsonRemove = function jsonRemove(filePath, that) {
+ Utils.jsonRemove = function jsonRemove(filePath) {
delete self.fakeContents["weave/" + filePath + ".json"];
return Promise.resolve();
};
@@ -79,19 +79,17 @@ export function FakeCryptoService() {
delete Weave.Crypto; // get rid of the getter first
Weave.Crypto = this;
- RawCryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC(
- keyBundle
- ) {
+ RawCryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC() {
return fakeSHA256HMAC(this.ciphertext);
};
}
FakeCryptoService.prototype = {
- async encrypt(clearText, symmetricKey, iv) {
+ async encrypt(clearText) {
return clearText;
},
- async decrypt(cipherText, symmetricKey, iv) {
+ async decrypt(cipherText) {
return cipherText;
},
@@ -104,7 +102,7 @@ FakeCryptoService.prototype = {
return btoa("fake-fake-fake-random-iv");
},
- expandData: function expandData(data, len) {
+ expandData: function expandData(data) {
return data;
},
diff --git a/services/sync/modules-testing/fxa_utils.sys.mjs b/services/sync/modules-testing/fxa_utils.sys.mjs
index c953f0eaa3..fdb9e261c9 100644
--- a/services/sync/modules-testing/fxa_utils.sys.mjs
+++ b/services/sync/modules-testing/fxa_utils.sys.mjs
@@ -23,7 +23,7 @@ export var initializeIdentityWithTokenServerResponse = function (response) {
}
// A mock request object.
- function MockRESTRequest(url) {}
+ function MockRESTRequest() {}
MockRESTRequest.prototype = {
_log: requestLog,
setHeader() {},
diff --git a/services/sync/modules/UIState.sys.mjs b/services/sync/modules/UIState.sys.mjs
index 8981d81f7d..6a45130cb1 100644
--- a/services/sync/modules/UIState.sys.mjs
+++ b/services/sync/modules/UIState.sys.mjs
@@ -87,7 +87,7 @@ const UIStateInternal = {
this._initialized = false;
},
- observe(subject, topic, data) {
+ observe(subject, topic) {
switch (topic) {
case "weave:service:sync:start":
this.toggleSyncActivity(true);
diff --git a/services/sync/modules/bridged_engine.sys.mjs b/services/sync/modules/bridged_engine.sys.mjs
index 45e5f685cd..3e40a80505 100644
--- a/services/sync/modules/bridged_engine.sys.mjs
+++ b/services/sync/modules/bridged_engine.sys.mjs
@@ -43,7 +43,7 @@ class BridgedStore {
this._batchChunkSize = 500;
}
- async applyIncomingBatch(records, countTelemetry) {
+ async applyIncomingBatch(records) {
for (let chunk of lazy.PlacesUtils.chunkArray(
records,
this._batchChunkSize
@@ -145,7 +145,7 @@ class InterruptedError extends Error {
/**
* Adapts a `Log.sys.mjs` logger to a `mozIServicesLogSink`. This class is copied
- * from `SyncedBookmarksMirror.jsm`.
+ * from `SyncedBookmarksMirror.sys.mjs`.
*/
export class LogAdapter {
constructor(log) {
diff --git a/services/sync/modules/collection_validator.sys.mjs b/services/sync/modules/collection_validator.sys.mjs
index a64ede10e9..1f40110ca9 100644
--- a/services/sync/modules/collection_validator.sys.mjs
+++ b/services/sync/modules/collection_validator.sys.mjs
@@ -114,13 +114,13 @@ export class CollectionValidator {
// Return whether or not a server item should be present on the client. Expected
// to be overridden.
- clientUnderstands(item) {
+ clientUnderstands() {
return true;
}
// Return whether or not a client item should be present on the server. Expected
// to be overridden
- async syncedByClient(item) {
+ async syncedByClient() {
return true;
}
diff --git a/services/sync/modules/constants.sys.mjs b/services/sync/modules/constants.sys.mjs
index 35c0ac2f0b..958e3345e6 100644
--- a/services/sync/modules/constants.sys.mjs
+++ b/services/sync/modules/constants.sys.mjs
@@ -4,7 +4,7 @@
// Don't manually modify this line, as it is automatically replaced on merge day
// by the gecko_migration.py script.
-export const WEAVE_VERSION = "1.126.0";
+export const WEAVE_VERSION = "1.127.0";
// Sync Server API version that the client supports.
export const SYNC_API_VERSION = "1.5";
diff --git a/services/sync/modules/engines.sys.mjs b/services/sync/modules/engines.sys.mjs
index 0d490ac4b3..63b4c02cc5 100644
--- a/services/sync/modules/engines.sys.mjs
+++ b/services/sync/modules/engines.sys.mjs
@@ -113,12 +113,12 @@ Tracker.prototype = {
},
// Also unsupported.
- async addChangedID(id, when) {
+ async addChangedID() {
throw new TypeError("Can't add changed ID to this tracker");
},
// Ditto.
- async removeChangedID(...ids) {
+ async removeChangedID() {
throw new TypeError("Can't remove changed IDs from this tracker");
},
@@ -155,7 +155,7 @@ Tracker.prototype = {
// Override these in your subclasses.
onStart() {},
onStop() {},
- async observe(subject, topic, data) {},
+ async observe() {},
engineIsEnabled() {
if (!this.engine) {
@@ -437,7 +437,7 @@ Store.prototype = {
* @param record
* The store record to create an item from
*/
- async create(record) {
+ async create() {
throw new Error("override create in a subclass");
},
@@ -450,7 +450,7 @@ Store.prototype = {
* @param record
* The store record to delete an item from
*/
- async remove(record) {
+ async remove() {
throw new Error("override remove in a subclass");
},
@@ -463,7 +463,7 @@ Store.prototype = {
* @param record
* The record to use to update an item from
*/
- async update(record) {
+ async update() {
throw new Error("override update in a subclass");
},
@@ -477,7 +477,7 @@ Store.prototype = {
* string record ID
* @return boolean indicating whether record exists locally
*/
- async itemExists(id) {
+ async itemExists() {
throw new Error("override itemExists in a subclass");
},
@@ -495,7 +495,7 @@ Store.prototype = {
* constructor for the newly-created record.
* @return record type for this engine
*/
- async createRecord(id, collection) {
+ async createRecord() {
throw new Error("override createRecord in a subclass");
},
@@ -507,7 +507,7 @@ Store.prototype = {
* @param newID
* string new record ID
*/
- async changeItemID(oldID, newID) {
+ async changeItemID() {
throw new Error("override changeItemID in a subclass");
},
@@ -1040,7 +1040,7 @@ SyncEngine.prototype = {
* Note: Overriding engines must take resyncs into account -- score will not
* be cleared.
*/
- shouldSkipSync(syncReason) {
+ shouldSkipSync() {
return false;
},
@@ -1550,7 +1550,7 @@ SyncEngine.prototype = {
// Indicates whether an incoming item should be deleted from the server at
// the end of the sync. Engines can override this method to clean up records
// that shouldn't be on the server.
- _shouldDeleteRemotely(remoteItem) {
+ _shouldDeleteRemotely() {
return false;
},
@@ -1560,7 +1560,7 @@ SyncEngine.prototype = {
*
* @return GUID of the similar item; falsy otherwise
*/
- async _findDupe(item) {
+ async _findDupe() {
// By default, assume there's no dupe items for the engine
},
@@ -1568,7 +1568,7 @@ SyncEngine.prototype = {
* Called before a remote record is discarded due to failed reconciliation.
* Used by bookmark sync to merge folder child orders.
*/
- beforeRecordDiscard(localRecord, remoteRecord, remoteIsNewer) {},
+ beforeRecordDiscard() {},
// Called when the server has a record marked as deleted, but locally we've
// changed it more recently than the deletion. If we return false, the
@@ -1576,7 +1576,7 @@ SyncEngine.prototype = {
// record to the server -- any extra work that's needed as part of this
// process should be done at this point (such as mark the record's parent
// for reuploading in the case of bookmarks).
- async _shouldReviveRemotelyDeletedRecord(remoteItem) {
+ async _shouldReviveRemotelyDeletedRecord() {
return true;
},
@@ -1948,7 +1948,7 @@ SyncEngine.prototype = {
}
},
- async _onRecordsWritten(succeeded, failed, serverModifiedTime) {
+ async _onRecordsWritten() {
// Implement this method to take specific actions against successfully
// uploaded records and failed records.
},
diff --git a/services/sync/modules/engines/bookmarks.sys.mjs b/services/sync/modules/engines/bookmarks.sys.mjs
index 3c1396f67d..4995da6899 100644
--- a/services/sync/modules/engines/bookmarks.sys.mjs
+++ b/services/sync/modules/engines/bookmarks.sys.mjs
@@ -513,7 +513,7 @@ BookmarksEngine.prototype = {
await this._apply();
},
- async _reconcile(item) {
+ async _reconcile() {
return true;
},
@@ -752,7 +752,7 @@ BookmarksStore.prototype = {
});
},
- async applyIncomingBatch(records, countTelemetry) {
+ async applyIncomingBatch(records) {
let buf = await this.ensureOpenMirror();
for (let chunk of lazy.PlacesUtils.chunkArray(
records,
@@ -921,11 +921,11 @@ Object.setPrototypeOf(BookmarksTracker.prototype, Tracker.prototype);
class BookmarksChangeset extends Changeset {
// Only `_reconcile` calls `getModifiedTimestamp` and `has`, and the engine
// does its own reconciliation.
- getModifiedTimestamp(id) {
+ getModifiedTimestamp() {
throw new Error("Don't use timestamps to resolve bookmark conflicts");
}
- has(id) {
+ has() {
throw new Error("Don't use the changeset to resolve bookmark conflicts");
}
diff --git a/services/sync/modules/engines/clients.sys.mjs b/services/sync/modules/engines/clients.sys.mjs
index eda92bd75b..cb391982e0 100644
--- a/services/sync/modules/engines/clients.sys.mjs
+++ b/services/sync/modules/engines/clients.sys.mjs
@@ -1107,7 +1107,7 @@ ClientsTracker.prototype = {
Svc.Obs.remove("fxaccounts:new_device_id", this.asyncObserver);
},
- async observe(subject, topic, data) {
+ async observe(subject, topic) {
switch (topic) {
case "nsPref:changed":
this._log.debug("client.name preference changed");
diff --git a/services/sync/modules/engines/extension-storage.sys.mjs b/services/sync/modules/engines/extension-storage.sys.mjs
index d2671978c8..693d94f647 100644
--- a/services/sync/modules/engines/extension-storage.sys.mjs
+++ b/services/sync/modules/engines/extension-storage.sys.mjs
@@ -124,7 +124,7 @@ ExtensionStorageEngineBridge.prototype = {
},
_takeMigrationInfo() {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
this.component
.QueryInterface(Ci.mozIExtensionStorageArea)
.takeMigrationInfo({
@@ -291,7 +291,7 @@ ExtensionStorageTracker.prototype = {
lazy.Svc.Obs.remove("ext.storage.sync-changed", this.asyncObserver);
},
- async observe(subject, topic, data) {
+ async observe(subject, topic) {
if (this.ignoreAll) {
return;
}
diff --git a/services/sync/modules/engines/forms.sys.mjs b/services/sync/modules/engines/forms.sys.mjs
index 3516327659..0d63eb96d1 100644
--- a/services/sync/modules/engines/forms.sys.mjs
+++ b/services/sync/modules/engines/forms.sys.mjs
@@ -189,7 +189,7 @@ FormStore.prototype = {
await this._processChange(change);
},
- async update(record) {
+ async update() {
this._log.trace("Ignoring form record update request!");
},
diff --git a/services/sync/modules/engines/prefs.sys.mjs b/services/sync/modules/engines/prefs.sys.mjs
index f29a9e7b59..cb494ec70e 100644
--- a/services/sync/modules/engines/prefs.sys.mjs
+++ b/services/sync/modules/engines/prefs.sys.mjs
@@ -386,7 +386,7 @@ PrefStore.prototype = {
return allprefs;
},
- async changeItemID(oldID, newID) {
+ async changeItemID() {
this._log.trace("PrefStore GUID is constant!");
},
@@ -406,11 +406,11 @@ PrefStore.prototype = {
return record;
},
- async create(record) {
+ async create() {
this._log.trace("Ignoring create request");
},
- async remove(record) {
+ async remove() {
this._log.trace("Ignoring remove request");
},
diff --git a/services/sync/modules/engines/tabs.sys.mjs b/services/sync/modules/engines/tabs.sys.mjs
index 861e051d1a..93747665f2 100644
--- a/services/sync/modules/engines/tabs.sys.mjs
+++ b/services/sync/modules/engines/tabs.sys.mjs
@@ -430,7 +430,7 @@ export const TabProvider = {
.then(iconData => {
thisTab.icon = iconData.uri.spec;
})
- .catch(ex => {
+ .catch(() => {
log.trace(
`Failed to fetch favicon for ${url}`,
thisTab.urlHistory[0]
@@ -503,7 +503,7 @@ TabTracker.prototype = {
}
},
- async observe(subject, topic, data) {
+ async observe(subject, topic) {
switch (topic) {
case "domwindowopened":
let onLoad = () => {
diff --git a/services/sync/modules/record.sys.mjs b/services/sync/modules/record.sys.mjs
index 7d5918a8ca..f8580cfbd4 100644
--- a/services/sync/modules/record.sys.mjs
+++ b/services/sync/modules/record.sys.mjs
@@ -182,7 +182,7 @@ RawCryptoWrapper.prototype = {
* @param {Cleartext} outgoingCleartext The cleartext to upload.
* @returns {String} The serialized cleartext.
*/
- transformBeforeEncrypt(outgoingCleartext) {
+ transformBeforeEncrypt() {
throw new TypeError("Override to stringify outgoing records");
},
@@ -194,7 +194,7 @@ RawCryptoWrapper.prototype = {
* @param {String} incomingCleartext The decrypted cleartext string.
* @returns {Cleartext} The parsed cleartext.
*/
- transformAfterDecrypt(incomingCleartext) {
+ transformAfterDecrypt() {
throw new TypeError("Override to parse incoming records");
},
@@ -527,7 +527,7 @@ CollectionKeyManager.prototype = {
/**
* Create a WBO for the current keys.
*/
- asWBO(collection, id) {
+ asWBO() {
return this._makeWBO(this._collections, this._default);
},
diff --git a/services/sync/modules/sync_auth.sys.mjs b/services/sync/modules/sync_auth.sys.mjs
index 6b8da4061c..cfa76827d5 100644
--- a/services/sync/modules/sync_auth.sys.mjs
+++ b/services/sync/modules/sync_auth.sys.mjs
@@ -164,7 +164,7 @@ SyncAuthManager.prototype = {
this._token = null;
},
- async observe(subject, topic, data) {
+ async observe(subject, topic) {
this._log.debug("observed " + topic);
if (!this.username) {
this._log.info("Sync is not configured, so ignoring the notification");
@@ -276,7 +276,7 @@ SyncAuthManager.prototype = {
* allows us to avoid a network request for when we actually need the
* migration info.
*/
- prefetchMigrationSentinel(service) {
+ prefetchMigrationSentinel() {
// nothing to do here until we decide to migrate away from FxA.
},
@@ -387,22 +387,28 @@ SyncAuthManager.prototype = {
// Do the token dance, with a retry in case of transient auth failure.
// We need to prove that we know the sync key in order to get a token
// from the tokenserver.
- let getToken = async key => {
+ let getToken = async (key, accessToken) => {
this._log.info("Getting a sync token from", this._tokenServerUrl);
- let token = await this._fetchTokenUsingOAuth(key);
+ let token = await this._fetchTokenUsingOAuth(key, accessToken);
this._log.trace("Successfully got a token");
return token;
};
+ const ttl = fxAccountsCommon.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS;
try {
let token, key;
try {
this._log.info("Getting sync key");
- key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
+ const tokenAndKey = await fxa.getOAuthTokenAndKey({
+ scope: SCOPE_OLD_SYNC,
+ ttl,
+ });
+
+ key = tokenAndKey.key;
if (!key) {
throw new Error("browser does not have the sync key, cannot sync");
}
- token = await getToken(key);
+ token = await getToken(key, tokenAndKey.token);
} catch (err) {
// If we get a 401 fetching the token it may be that our auth tokens needed
// to be regenerated; retry exactly once.
@@ -412,8 +418,11 @@ SyncAuthManager.prototype = {
this._log.warn(
"Token server returned 401, retrying token fetch with fresh credentials"
);
- key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
- token = await getToken(key);
+ const tokenAndKey = await fxa.getOAuthTokenAndKey({
+ scope: SCOPE_OLD_SYNC,
+ ttl,
+ });
+ token = await getToken(tokenAndKey.key, tokenAndKey.token);
}
// TODO: Make it be only 80% of the duration, so refresh the token
// before it actually expires. This is to avoid sync storage errors
@@ -437,7 +446,7 @@ SyncAuthManager.prototype = {
// A hawkclient error.
} else if (err.code && err.code === 401) {
err = new AuthenticationError(err, "hawkclient");
- // An FxAccounts.jsm error.
+ // An FxAccounts.sys.mjs error.
} else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
err = new AuthenticationError(err, "fxaccounts");
}
@@ -460,17 +469,13 @@ SyncAuthManager.prototype = {
},
/**
- * Generates an OAuth access_token using the OLD_SYNC scope and exchanges it
- * for a TokenServer token.
- *
+ * Exchanges an OAuth access_token for a TokenServer token.
* @returns {Promise}
* @private
*/
- async _fetchTokenUsingOAuth(key) {
+ async _fetchTokenUsingOAuth(key, accessToken) {
this._log.debug("Getting a token using OAuth");
const fxa = this._fxaService;
- const ttl = fxAccountsCommon.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS;
- const accessToken = await fxa.getOAuthToken({ scope: SCOPE_OLD_SYNC, ttl });
const headers = {
"X-KeyId": key.kid,
};
diff --git a/services/sync/modules/telemetry.sys.mjs b/services/sync/modules/telemetry.sys.mjs
index c08f405b0e..28888ef277 100644
--- a/services/sync/modules/telemetry.sys.mjs
+++ b/services/sync/modules/telemetry.sys.mjs
@@ -241,10 +241,14 @@ export class ErrorSanitizer {
NotAllowedError: this.E_PERMISSION_DENIED,
};
+ // IOUtils error messages include the specific nsresult error code that caused them.
+ static NS_ERROR_RE = new RegExp(/ \(NS_ERROR_.*\)$/);
+
static #cleanOSErrorMessage(message, error = undefined) {
if (DOMException.isInstance(error)) {
const sub = this.DOMErrorSubstitutions[error.name];
message = message.replaceAll("\\", "/");
+ message = message.replace(this.NS_ERROR_RE, "");
if (sub) {
return `${sub} ${message}`;
}
diff --git a/services/sync/tests/tps/.eslintrc.js b/services/sync/tests/tps/.eslintrc.js
index 182e87933b..63c8344934 100644
--- a/services/sync/tests/tps/.eslintrc.js
+++ b/services/sync/tests/tps/.eslintrc.js
@@ -2,7 +2,7 @@
module.exports = {
globals: {
- // Injected into tests via tps.jsm
+ // Injected into tests via tps.sys.mjs
Addons: false,
Addresses: false,
Bookmarks: false,
diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js
index e79e55e57f..865117e5d2 100644
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -530,8 +530,8 @@ async function sync_engine_and_validate_telem(
// Returns a promise that resolves once the specified observer notification
// has fired.
-function promiseOneObserver(topic, callback) {
- return new Promise((resolve, reject) => {
+function promiseOneObserver(topic) {
+ return new Promise(resolve => {
let observer = function (subject, data) {
Svc.Obs.remove(topic, observer);
resolve({ subject, data });
diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js
index 84dbb33951..d8603465c1 100644
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -687,8 +687,8 @@ function track_collections_helper() {
* prototype, and override as appropriate.
*/
var SyncServerCallback = {
- onCollectionDeleted: function onCollectionDeleted(user, collection) {},
- onItemDeleted: function onItemDeleted(user, collection, wboID) {},
+ onCollectionDeleted: function onCollectionDeleted() {},
+ onItemDeleted: function onItemDeleted() {},
/**
* Called at the top of every request.
@@ -699,7 +699,7 @@ var SyncServerCallback = {
* must be taken to not screw with the response body or headers that may
* conflict with normal operation of this server.
*/
- onRequest: function onRequest(request, response) {},
+ onRequest: function onRequest() {},
};
/**
diff --git a/services/sync/tests/unit/test_addon_utils.js b/services/sync/tests/unit/test_addon_utils.js
index c039bee16c..e9e49fb9ea 100644
--- a/services/sync/tests/unit/test_addon_utils.js
+++ b/services/sync/tests/unit/test_addon_utils.js
@@ -119,7 +119,7 @@ add_task(async function test_source_uri_rewrite() {
let installCalled = false;
Object.getPrototypeOf(AddonUtils).installAddonFromSearchResult =
- async function testInstallAddon(addon, metadata) {
+ async function testInstallAddon(addon) {
Assert.equal(
SERVER_ADDRESS + "/require.xpi?src=sync",
addon.sourceURI.spec
diff --git a/services/sync/tests/unit/test_addons_validator.js b/services/sync/tests/unit/test_addons_validator.js
index 60f2f8bf43..91f3f7b31b 100644
--- a/services/sync/tests/unit/test_addons_validator.js
+++ b/services/sync/tests/unit/test_addons_validator.js
@@ -49,7 +49,7 @@ function getDummyServerAndClient() {
add_task(async function test_valid() {
let { server, client } = getDummyServerAndClient();
let validator = new AddonValidator({
- _findDupe(item) {
+ _findDupe() {
return null;
},
isAddonSyncable(item) {
diff --git a/services/sync/tests/unit/test_bookmark_engine.js b/services/sync/tests/unit/test_bookmark_engine.js
index 6274a6b836..2f5ac9dcd3 100644
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -940,7 +940,7 @@ add_bookmark_test(async function test_sync_dateAdded(engine) {
// Make sure it's within 24 hours of the right timestamp... This is a little
// dodgey but we only really care that it's basically accurate and has the
// right day.
- ok(Math.abs(Date.now() - record3.dateAdded) < 24 * 60 * 60 * 1000);
+ Assert.less(Math.abs(Date.now() - record3.dateAdded), 24 * 60 * 60 * 1000);
let record4 = await store.createRecord(item4GUID);
equal(
diff --git a/services/sync/tests/unit/test_bookmark_tracker.js b/services/sync/tests/unit/test_bookmark_tracker.js
index 9cfbb4de78..8c26232cd5 100644
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -54,8 +54,16 @@ async function verifyTrackedItems(tracked) {
let trackedIDs = new Set(Object.keys(changedIDs));
for (let guid of tracked) {
ok(guid in changedIDs, `${guid} should be tracked`);
- ok(changedIDs[guid].modified > 0, `${guid} should have a modified time`);
- ok(changedIDs[guid].counter >= -1, `${guid} should have a change counter`);
+ Assert.greater(
+ changedIDs[guid].modified,
+ 0,
+ `${guid} should have a modified time`
+ );
+ Assert.greaterOrEqual(
+ changedIDs[guid].counter,
+ -1,
+ `${guid} should have a change counter`
+ );
trackedIDs.delete(guid);
}
equal(
@@ -770,7 +778,7 @@ add_task(async function test_onFaviconChanged() {
iconURI,
true,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
- (uri, dataLen, data, mimeType) => {
+ () => {
resolve();
},
Services.scriptSecurityManager.getSystemPrincipal()
diff --git a/services/sync/tests/unit/test_clients_engine.js b/services/sync/tests/unit/test_clients_engine.js
index d910a67503..9a5115db4e 100644
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -125,7 +125,7 @@ add_task(async function test_bad_hmac() {
check_clients_count(0);
await syncClientsEngine(server);
check_clients_count(1);
- ok(engine.lastRecordUpload > 0);
+ Assert.greater(engine.lastRecordUpload, 0);
ok(!engine.isFirstSync);
// Our uploaded record has a version.
@@ -275,7 +275,7 @@ add_task(async function test_full_sync() {
strictEqual(engine.lastRecordUpload, 0);
ok(engine.isFirstSync);
await syncClientsEngine(server);
- ok(engine.lastRecordUpload > 0);
+ Assert.greater(engine.lastRecordUpload, 0);
ok(!engine.isFirstSync);
deepEqual(
user.collection("clients").keys().sort(),
@@ -333,7 +333,7 @@ add_task(async function test_sync() {
ok(engine.isFirstSync);
await syncClientsEngine(server);
ok(!!clientWBO().payload);
- ok(engine.lastRecordUpload > 0);
+ Assert.greater(engine.lastRecordUpload, 0);
ok(!engine.isFirstSync);
_(
@@ -344,7 +344,7 @@ add_task(async function test_sync() {
clientWBO().payload = undefined;
await syncClientsEngine(server);
ok(!!clientWBO().payload);
- ok(engine.lastRecordUpload > lastweek);
+ Assert.greater(engine.lastRecordUpload, lastweek);
ok(!engine.isFirstSync);
_("Remove client record.");
@@ -394,8 +394,8 @@ add_task(async function test_client_name_change() {
changedIDs = await tracker.getChangedIDs();
equal(Object.keys(changedIDs).length, 1);
ok(engine.localID in changedIDs);
- ok(tracker.score > initialScore);
- ok(tracker.score >= SCORE_INCREMENT_XLARGE);
+ Assert.greater(tracker.score, initialScore);
+ Assert.greaterOrEqual(tracker.score, SCORE_INCREMENT_XLARGE);
await tracker.stop();
@@ -425,8 +425,8 @@ add_task(async function test_fxa_device_id_change() {
changedIDs = await tracker.getChangedIDs();
equal(Object.keys(changedIDs).length, 1);
ok(engine.localID in changedIDs);
- ok(tracker.score > initialScore);
- ok(tracker.score >= SINGLE_USER_THRESHOLD);
+ Assert.greater(tracker.score, initialScore);
+ Assert.greaterOrEqual(tracker.score, SINGLE_USER_THRESHOLD);
await tracker.stop();
@@ -477,7 +477,10 @@ add_task(async function test_last_modified() {
await engine._uploadOutgoing();
_("Local record should have updated timestamp");
- ok(engine._store._remoteClients[activeID].serverLastModified >= now);
+ Assert.greaterOrEqual(
+ engine._store._remoteClients[activeID].serverLastModified,
+ now
+ );
_("Record on the server should have new name but not serverLastModified");
let payload = collection.cleartext(activeID);
@@ -732,7 +735,7 @@ add_task(async function test_filter_duplicate_names() {
strictEqual(engine.lastRecordUpload, 0);
ok(engine.isFirstSync);
await syncClientsEngine(server);
- ok(engine.lastRecordUpload > 0);
+ Assert.greater(engine.lastRecordUpload, 0);
ok(!engine.isFirstSync);
deepEqual(
user.collection("clients").keys().sort(),
@@ -776,7 +779,7 @@ add_task(async function test_filter_duplicate_names() {
// Check that a subsequent Sync doesn't report anything as being processed.
let counts;
- Svc.Obs.add("weave:engine:sync:applied", function observe(subject, data) {
+ Svc.Obs.add("weave:engine:sync:applied", function observe(subject) {
Svc.Obs.remove("weave:engine:sync:applied", observe);
counts = subject;
});
@@ -915,7 +918,7 @@ add_task(async function test_command_sync() {
_("Checking record was uploaded.");
notEqual(clientWBO(engine.localID).payload, undefined);
- ok(engine.lastRecordUpload > 0);
+ Assert.greater(engine.lastRecordUpload, 0);
ok(!engine.isFirstSync);
notEqual(clientWBO(remoteId).payload, undefined);
diff --git a/services/sync/tests/unit/test_declined.js b/services/sync/tests/unit/test_declined.js
index af7f8eb8c5..aecd33ee6e 100644
--- a/services/sync/tests/unit/test_declined.js
+++ b/services/sync/tests/unit/test_declined.js
@@ -79,7 +79,7 @@ add_task(async function testOldMeta() {
let declinedEngines = new DeclinedEngines(Service);
- function onNotDeclined(subject, topic, data) {
+ function onNotDeclined(subject) {
Observers.remove("weave:engines:notdeclined", onNotDeclined);
Assert.ok(
subject.undecided.has("actual"),
@@ -129,7 +129,7 @@ add_task(async function testDeclinedMeta() {
let declinedEngines = new DeclinedEngines(Service);
- function onNotDeclined(subject, topic, data) {
+ function onNotDeclined(subject) {
Observers.remove("weave:engines:notdeclined", onNotDeclined);
Assert.ok(
subject.undecided.has("actual"),
diff --git a/services/sync/tests/unit/test_engine_abort.js b/services/sync/tests/unit/test_engine_abort.js
index f9bbf9d338..a7c62afb4a 100644
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -37,7 +37,7 @@ add_task(async function test_processIncoming_abort() {
);
meta_global.payload.engines = { rotary: { version: engine.version, syncID } };
_("Fake applyIncoming to abort.");
- engine._store.applyIncoming = async function (record) {
+ engine._store.applyIncoming = async function () {
let ex = {
code: SyncEngine.prototype.eEngineAbortApplyIncoming,
cause: "Nooo",
diff --git a/services/sync/tests/unit/test_errorhandler_1.js b/services/sync/tests/unit/test_errorhandler_1.js
index 2d52b93a02..f5e96ed44e 100644
--- a/services/sync/tests/unit/test_errorhandler_1.js
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -286,13 +286,10 @@ add_task(async function test_info_collections_login_server_maintenance_error() {
await configureIdentity({ username: "broken.info" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -318,13 +315,10 @@ add_task(async function test_meta_global_login_server_maintenance_error() {
await configureIdentity({ username: "broken.meta" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
diff --git a/services/sync/tests/unit/test_errorhandler_2.js b/services/sync/tests/unit/test_errorhandler_2.js
index 5cab4d832d..2a7e6ba619 100644
--- a/services/sync/tests/unit/test_errorhandler_2.js
+++ b/services/sync/tests/unit/test_errorhandler_2.js
@@ -74,13 +74,10 @@ add_task(async function test_crypto_keys_login_server_maintenance_error() {
Service.collectionKeys.clear();
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -178,13 +175,10 @@ add_task(
await configureIdentity({ username: "broken.info" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -215,13 +209,10 @@ add_task(
await configureIdentity({ username: "broken.meta" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -254,13 +245,10 @@ add_task(
Service.collectionKeys.clear();
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -291,13 +279,10 @@ add_task(
await configureIdentity({ username: "broken.keys" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -328,13 +313,10 @@ add_task(
await configureIdentity({ username: "broken.wipe" }, server);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
@@ -368,13 +350,10 @@ add_task(
engine.enabled = true;
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.ok(!Status.enforceBackoff);
Assert.equal(Status.service, STATUS_OK);
diff --git a/services/sync/tests/unit/test_errorhandler_filelog.js b/services/sync/tests/unit/test_errorhandler_filelog.js
index 66260b3f59..357049276c 100644
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -82,7 +82,7 @@ function readFile(file, callback) {
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true,
},
- function (inputStream, statusCode, request) {
+ function (inputStream, statusCode) {
let data = NetUtil.readInputStreamToString(
inputStream,
inputStream.available()
diff --git a/services/sync/tests/unit/test_fxa_node_reassignment.js b/services/sync/tests/unit/test_fxa_node_reassignment.js
index 0b25df0183..0fa7ee922c 100644
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -46,7 +46,7 @@ function prepareServer(cbAfterTokenFetch) {
// A server callback to ensure we don't accidentally hit the wrong endpoint
// after a node reassignment.
let callback = {
- onRequest(req, resp) {
+ onRequest(req) {
let full = `${req.scheme}://${req.host}:${req.port}${req.path}`;
let expected = config.fxaccount.token.endpoint;
Assert.ok(
diff --git a/services/sync/tests/unit/test_history_engine.js b/services/sync/tests/unit/test_history_engine.js
index 9cca379b0b..259338df09 100644
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -16,13 +16,13 @@ XPCOMUtils.defineLazyServiceGetter(
"mozIAsyncHistory"
);
async function rawAddVisit(id, uri, visitPRTime, transitionType) {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
let results = [];
let handler = {
handleResult(result) {
results.push(result);
},
- handleError(resultCode, placeInfo) {
+ handleError(resultCode) {
do_throw(`updatePlaces gave error ${resultCode}!`);
},
handleCompletion(count) {
diff --git a/services/sync/tests/unit/test_history_store.js b/services/sync/tests/unit/test_history_store.js
index 07aee0dd01..1777664bb6 100644
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -17,7 +17,7 @@ const TIMESTAMP3 = (Date.now() - 123894) * 1000;
function promiseOnVisitObserved() {
return new Promise(res => {
- let listener = new PlacesWeakCallbackWrapper(events => {
+ let listener = new PlacesWeakCallbackWrapper(() => {
PlacesObservers.removeListener(["page-visited"], listener);
res();
});
diff --git a/services/sync/tests/unit/test_history_tracker.js b/services/sync/tests/unit/test_history_tracker.js
index 6f351d6984..64433574b4 100644
--- a/services/sync/tests/unit/test_history_tracker.js
+++ b/services/sync/tests/unit/test_history_tracker.js
@@ -37,7 +37,7 @@ async function verifyTrackedItems(tracked) {
let trackedIDs = new Set(Object.keys(changes));
for (let guid of tracked) {
ok(guid in changes, `${guid} should be tracked`);
- ok(changes[guid] > 0, `${guid} should have a modified time`);
+ Assert.greater(changes[guid], 0, `${guid} should have a modified time`);
trackedIDs.delete(guid);
}
equal(
@@ -160,7 +160,7 @@ add_task(async function test_dont_track_expiration() {
let scorePromise = promiseOneObserver("weave:engine:score:updated");
// Observe expiration.
- Services.obs.addObserver(function onExpiration(aSubject, aTopic, aData) {
+ Services.obs.addObserver(function onExpiration(aSubject, aTopic) {
Services.obs.removeObserver(onExpiration, aTopic);
// Remove the remaining page to update its score.
PlacesUtils.history.remove(uriToRemove);
diff --git a/services/sync/tests/unit/test_hmac_error.js b/services/sync/tests/unit/test_hmac_error.js
index 26dbc12dea..a04e54f476 100644
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -171,7 +171,7 @@ add_task(async function hmac_error_during_node_reassignment() {
}
let onSyncFinished = function () {};
let obs = {
- observe: function observe(subject, topic, data) {
+ observe: function observe(subject, topic) {
switch (topic) {
case "weave:service:sync:error":
onSyncError();
diff --git a/services/sync/tests/unit/test_httpd_sync_server.js b/services/sync/tests/unit/test_httpd_sync_server.js
index 6ac8ff5e04..23bb05b15d 100644
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -160,7 +160,7 @@ add_task(async function test_storage_request() {
async function deleteWBONotExists() {
let req = localRequest(server, keysURL);
- server.callback.onItemDeleted = function (username, collection, wboID) {
+ server.callback.onItemDeleted = function () {
do_throw("onItemDeleted should not have been called.");
};
diff --git a/services/sync/tests/unit/test_interval_triggers.js b/services/sync/tests/unit/test_interval_triggers.js
index 6f2821ec45..eb0b39f636 100644
--- a/services/sync/tests/unit/test_interval_triggers.js
+++ b/services/sync/tests/unit/test_interval_triggers.js
@@ -51,7 +51,7 @@ add_task(async function setup() {
// Don't remove stale clients when syncing. This is a test-only workaround
// that lets us add clients directly to the store, without losing them on
// the next sync.
- clientsEngine._removeRemoteClient = async id => {};
+ clientsEngine._removeRemoteClient = async () => {};
});
add_task(async function test_successful_sync_adjustSyncInterval() {
diff --git a/services/sync/tests/unit/test_password_engine.js b/services/sync/tests/unit/test_password_engine.js
index 081403f63d..54fe8972f2 100644
--- a/services/sync/tests/unit/test_password_engine.js
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -434,8 +434,9 @@ add_task(async function test_sync_outgoing() {
equal(deletedLogin.guid, guid, "deleted login guid");
equal(deletedLogin.everSynced, true, "deleted login everSynced");
equal(deletedLogin.syncCounter, 0, "deleted login syncCounter");
- ok(
- deletedLogin.timePasswordChanged > 0,
+ Assert.greater(
+ deletedLogin.timePasswordChanged,
+ 0,
"deleted login timePasswordChanged"
);
} finally {
@@ -525,7 +526,7 @@ add_task(async function test_sync_incoming() {
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
- ok(logins[0].timePasswordChanged > details.timePasswordChanged);
+ Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
@@ -553,7 +554,7 @@ add_task(async function test_sync_incoming() {
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
- ok(logins[0].timePasswordChanged > details.timePasswordChanged);
+ Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js
index 5182784639..5dee57b39a 100644
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -480,7 +480,7 @@ add_task(async function test_post_override_content_type() {
add_task(async function test_weave_backoff() {
_("X-Weave-Backoff header notifies observer");
let backoffInterval;
- function onBackoff(subject, data) {
+ function onBackoff(subject) {
backoffInterval = subject;
}
Observers.add("weave:service:backoff:interval", onBackoff);
diff --git a/services/sync/tests/unit/test_service_sync_401.js b/services/sync/tests/unit/test_service_sync_401.js
index a0bde0b0ab..0c285872e9 100644
--- a/services/sync/tests/unit/test_service_sync_401.js
+++ b/services/sync/tests/unit/test_service_sync_401.js
@@ -48,7 +48,7 @@ add_task(async function run_test() {
Svc.PrefBranch.setIntPref("lastPing", Math.floor(Date.now() / 1000));
let threw = false;
- Svc.Obs.add("weave:service:sync:error", function (subject, data) {
+ Svc.Obs.add("weave:service:sync:error", function () {
threw = true;
});
diff --git a/services/sync/tests/unit/test_service_verifyLogin.js b/services/sync/tests/unit/test_service_verifyLogin.js
index b99b5c692c..b3afe6179a 100644
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -78,13 +78,10 @@ add_task(async function test_verifyLogin() {
Service._updateCachedURLs();
Assert.ok(!Service.status.enforceBackoff);
let backoffInterval;
- Svc.Obs.add(
- "weave:service:backoff:interval",
- function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- }
- );
+ Svc.Obs.add("weave:service:backoff:interval", function observe(subject) {
+ Svc.Obs.remove("weave:service:backoff:interval", observe);
+ backoffInterval = subject;
+ });
Assert.equal(false, await Service.verifyLogin());
Assert.ok(Service.status.enforceBackoff);
Assert.equal(backoffInterval, 42);
diff --git a/services/sync/tests/unit/test_sync_auth_manager.js b/services/sync/tests/unit/test_sync_auth_manager.js
index 9af40d26c6..f9fa669667 100644
--- a/services/sync/tests/unit/test_sync_auth_manager.js
+++ b/services/sync/tests/unit/test_sync_auth_manager.js
@@ -37,9 +37,8 @@ const { TokenServerClient, TokenServerClientServerError } =
ChromeUtils.importESModule(
"resource://services-common/tokenserverclient.sys.mjs"
);
-const { AccountState } = ChromeUtils.importESModule(
- "resource://gre/modules/FxAccounts.sys.mjs"
-);
+const { AccountState, ERROR_INVALID_ACCOUNT_STATE } =
+ ChromeUtils.importESModule("resource://gre/modules/FxAccounts.sys.mjs");
const SECOND_MS = 1000;
const MINUTE_MS = SECOND_MS * 60;
@@ -192,8 +191,11 @@ add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
await Assert.rejects(
syncAuthManager._ensureValidToken(),
- AuthenticationError,
- "should reject due to an auth error"
+ err => {
+ Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
+ return true; // expected error
+ },
+ "should reject because the account was deleted"
);
Assert.ok(accessTokenWithSessionTokenCalled);
@@ -801,14 +803,11 @@ add_task(async function test_getKeysMissing() {
storageManager.initialize(identityConfig.fxaccount.user);
return new AccountState(storageManager);
},
- // And the keys object with a mock that returns no keys.
- keys: {
- getKeyForScope() {
- return Promise.resolve(null);
- },
- },
});
-
+ fxa.getOAuthTokenAndKey = () => {
+ // And the keys object with a mock that returns no keys.
+ return Promise.resolve({ key: null, token: "fake token" });
+ };
syncAuthManager._fxaService = fxa;
await Assert.rejects(
@@ -844,14 +843,12 @@ add_task(async function test_getKeysUnexpecedError() {
storageManager.initialize(identityConfig.fxaccount.user);
return new AccountState(storageManager);
},
- // And the keys object with a mock that returns no keys.
- keys: {
- async getKeyForScope() {
- throw new Error("well that was unexpected");
- },
- },
});
+ fxa.getOAuthTokenAndKey = () => {
+ return Promise.reject("well that was unexpected");
+ };
+
syncAuthManager._fxaService = fxa;
await Assert.rejects(
@@ -1005,7 +1002,7 @@ function mockTokenServer(func) {
requestLog.addAppender(new Log.DumpAppender());
requestLog.level = Log.Level.Trace;
}
- function MockRESTRequest(url) {}
+ function MockRESTRequest() {}
MockRESTRequest.prototype = {
_log: requestLog,
setHeader() {},
diff --git a/services/sync/tests/unit/test_syncedtabs.js b/services/sync/tests/unit/test_syncedtabs.js
index 79ab3e0686..c915e12602 100644
--- a/services/sync/tests/unit/test_syncedtabs.js
+++ b/services/sync/tests/unit/test_syncedtabs.js
@@ -87,7 +87,7 @@ let MockClientsEngine = {
return tabsEngine.clients[id].fxaDeviceId;
},
- getClientType(id) {
+ getClientType() {
return "desktop";
},
};
diff --git a/services/sync/tests/unit/test_syncscheduler.js b/services/sync/tests/unit/test_syncscheduler.js
index 98b7937da3..8eb1ea3f40 100644
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -92,7 +92,7 @@ add_task(async function setup() {
// Don't remove stale clients when syncing. This is a test-only workaround
// that lets us add clients directly to the store, without losing them on
// the next sync.
- clientsEngine._removeRemoteClient = async id => {};
+ clientsEngine._removeRemoteClient = async () => {};
await Service.engineManager.clear();
validate_all_future_pings();
diff --git a/services/sync/tests/unit/test_tab_quickwrite.js b/services/sync/tests/unit/test_tab_quickwrite.js
index 2a1c75c8c6..c363992d66 100644
--- a/services/sync/tests/unit/test_tab_quickwrite.js
+++ b/services/sync/tests/unit/test_tab_quickwrite.js
@@ -179,7 +179,7 @@ add_task(async function test_tab_quickWrite_telemetry() {
let telem = get_sync_test_telemetry();
telem.payloads = [];
let oldSubmit = telem.submit;
- let submitPromise = new Promise((resolve, reject) => {
+ let submitPromise = new Promise(resolve => {
telem.submit = function (ping) {
telem.submit = oldSubmit;
resolve(ping);
diff --git a/services/sync/tests/unit/test_telemetry.js b/services/sync/tests/unit/test_telemetry.js
index 961e96a01b..4f3a4e7c2b 100644
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -734,7 +734,7 @@ add_task(async function test_clean_real_os_error() {
equal(failureReason.name, "unexpectederror");
equal(
failureReason.error,
- "OS error [File/Path not found] Could not open the file at [profileDir]/no/such/path.json"
+ "OS error [File/Path not found] Could not open `[profileDir]/no/such/path.json': file does not exist"
);
});
} finally {
@@ -1351,7 +1351,7 @@ add_task(async function test_no_node_type() {
await configureIdentity(null, server);
await sync_and_validate_telem(ping => {
- ok(ping.syncNodeType === undefined);
+ Assert.strictEqual(ping.syncNodeType, undefined);
}, true);
await promiseStopServer(server);
});
diff --git a/services/sync/tests/unit/test_uistate.js b/services/sync/tests/unit/test_uistate.js
index cb1ff1979e..136f274a71 100644
--- a/services/sync/tests/unit/test_uistate.js
+++ b/services/sync/tests/unit/test_uistate.js
@@ -292,7 +292,7 @@ add_task(async function test_syncFinished() {
const newState = Object.assign({}, UIState.get());
ok(!newState.syncing);
- ok(new Date(newState.lastSync) > new Date(oldState.lastSync));
+ Assert.greater(new Date(newState.lastSync), new Date(oldState.lastSync));
});
add_task(async function test_syncError() {
@@ -314,7 +314,7 @@ add_task(async function test_syncError() {
function observeUIUpdate() {
return new Promise(resolve => {
- let obs = (aSubject, aTopic, aData) => {
+ let obs = (aSubject, aTopic) => {
Services.obs.removeObserver(obs, aTopic);
const state = UIState.get();
resolve(state);
diff --git a/services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs b/services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs
index a7724c6aaa..b78e31ab79 100644
--- a/services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs
+++ b/services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs
@@ -602,7 +602,7 @@ class ServerRecordInspection {
await lazy.Async.yieldingForEach(
this.liveRecords,
- (record, i) => {
+ record => {
if (!seen.has(record.id)) {
// We intentionally don't record the parentid here, since we only record
// that if the record refers to a parent that doesn't exist, which we
diff --git a/services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs b/services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs
index 8ea8f3b780..5ac25dbb4c 100644
--- a/services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs
+++ b/services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs
@@ -44,7 +44,7 @@ export var BrowserTabs = {
// Wait for the tab to load.
await new Promise(resolve => {
let mm = browser.ownerGlobal.messageManager;
- mm.addMessageListener("tps:loadEvent", function onLoad(msg) {
+ mm.addMessageListener("tps:loadEvent", function onLoad() {
mm.removeMessageListener("tps:loadEvent", onLoad);
resolve();
});
diff --git a/services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs b/services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs
index b0798b9031..22c2f47ec9 100644
--- a/services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs
+++ b/services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs
@@ -16,7 +16,7 @@ export var BrowserWindows = {
* @param aPrivate The private option.
* @return nothing
*/
- Add(aPrivate, fn) {
+ Add(aPrivate) {
return new Promise(resolve => {
let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
let win = mainWindow.OpenBrowserWindow({ private: aPrivate });
diff --git a/services/sync/tps/extensions/tps/resource/tps.sys.mjs b/services/sync/tps/extensions/tps/resource/tps.sys.mjs
index 2c4a5994a6..449ca27411 100644
--- a/services/sync/tps/extensions/tps/resource/tps.sys.mjs
+++ b/services/sync/tps/extensions/tps/resource/tps.sys.mjs
@@ -168,7 +168,7 @@ export var TPS = {
"nsISupportsWeakReference",
]),
- observe: function TPS__observe(subject, topic, data) {
+ observe: function TPS__observe(subject, topic) {
try {
lazy.Logger.logInfo("----------event observed: " + topic);