diff options
Diffstat (limited to 'services')
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); |