1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * A worker dedicated to Remote Settings.
9 // These files are imported into the worker scope, and are not shared singletons
10 // with the main thread.
11 /* eslint-disable mozilla/reject-import-system-module-from-non-system */
12 import { CanonicalJSON } from "resource://gre/modules/CanonicalJSON.sys.mjs";
13 import { IDBHelpers } from "resource://services-settings/IDBHelpers.sys.mjs";
14 import { SharedUtils } from "resource://services-settings/SharedUtils.sys.mjs";
15 import { jsesc } from "resource://gre/modules/third_party/jsesc/jsesc.mjs";
16 /* eslint-enable mozilla/reject-import-system-module-from-non-system */
18 const IDB_RECORDS_STORE = "records";
19 const IDB_TIMESTAMPS_STORE = "timestamps";
21 let gShutdown = false;
25 * Return the canonical JSON serialization of the specified records.
26 * It has to match what is done on the server (See Kinto/kinto-signer).
28 * @param {Array<Object>} records
29 * @param {String} timestamp
32 async canonicalStringify(records, timestamp) {
33 // Sort list by record id.
34 let allRecords = records.sort((a, b) => {
38 return a.id > b.id ? 1 : 0;
40 // All existing records are replaced by the version from the server
41 // and deleted records are removed.
42 for (let i = 0; i < allRecords.length /* no increment! */; ) {
43 const rec = allRecords[i];
44 const next = allRecords[i + 1];
45 if ((next && rec.id == next.id) || rec.deleted) {
46 allRecords.splice(i, 1); // remove local record
52 last_modified: "" + timestamp,
55 return CanonicalJSON.stringify(toSerialize, jsesc);
59 * If present, import the JSON file into the Remote Settings IndexedDB
60 * for the specified bucket and collection.
61 * (eg. blocklists/certificates, main/onboarding)
62 * @param {String} bucket
63 * @param {String} collection
64 * @returns {int} Number of records loaded from dump or -1 if no dump found.
66 async importJSONDump(bucket, collection) {
67 const { data: records, timestamp } = await SharedUtils.loadJSONDump(
71 if (records === null) {
72 // Return -1 if file is missing.
76 throw new Error("Can't import when we've started shutting down.");
78 await importDumpIDB(bucket, collection, records, timestamp);
79 return records.length;
83 * Check that the specified file matches the expected size and SHA-256 hash.
84 * @param {String} fileUrl file URL to read from
85 * @param {Number} size expected file size
86 * @param {String} size expected file SHA-256 as hex string
89 async checkFileHash(fileUrl, size, hash) {
92 resp = await fetch(fileUrl);
94 // File does not exist.
97 const buffer = await resp.arrayBuffer();
98 return SharedUtils.checkContentHash(buffer, size, hash);
101 async prepareShutdown() {
103 // Ensure we can iterate and abort (which may delete items) by cloning
105 let transactions = Array.from(gPendingTransactions);
106 for (let transaction of transactions) {
110 // We can hit this case if the transaction has finished but
111 // we haven't heard about it yet.
116 _test_only_import(bucket, collection, records, timestamp) {
117 return importDumpIDB(bucket, collection, records, timestamp);
122 * Wrap worker invocations in order to return the `callbackId` along
123 * the result. This will allow to transform the worker invocations
124 * into promises in `RemoteSettingsWorker.sys.mjs`.
126 self.onmessage = event => {
127 const { callbackId, method, args = [] } = event.data;
128 Agent[method](...args)
130 self.postMessage({ callbackId, result });
133 console.log(`RemoteSettingsWorker error: ${error}`);
134 self.postMessage({ callbackId, error: "" + error });
138 let gPendingTransactions = new Set();
141 * Import the records into the Remote Settings Chrome IndexedDB.
143 * Note: This duplicates some logics from `kinto-offline-client.sys.mjs`.
145 * @param {String} bucket
146 * @param {String} collection
147 * @param {Array<Object>} records
148 * @param {Number} timestamp
150 async function importDumpIDB(bucket, collection, records, timestamp) {
151 // Open the DB. It will exist since if we are running this, it means
152 // we already tried to read the timestamp in `remote-settings.sys.mjs`
153 const db = await IDBHelpers.openIDB(false /* do not allow upgrades */);
155 // try...finally to ensure we always close the db.
158 throw new Error("Can't import when we've started shutting down.");
161 // Each entry of the dump will be stored in the records store.
162 // They are indexed by `_cid`.
163 const cid = bucket + "/" + collection;
164 // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump().
165 records.forEach(item => {
168 // Store the collection timestamp.
169 let { transaction, promise } = IDBHelpers.executeIDB(
171 [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE],
173 ([recordsStore, timestampStore], rejectTransaction) => {
174 // Wipe before loading
175 recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true));
176 IDBHelpers.bulkOperationHelper(
179 reject: rejectTransaction,
181 timestampStore.put({ cid, value: timestamp });
189 gPendingTransactions.add(transaction);
190 promise = promise.finally(() => gPendingTransactions.delete(transaction));
193 // Close now that we're done.