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/. */
5 /* eslint-env mozilla/chrome-worker */
10 * A worker dedicated to Remote Settings.
14 "resource://gre/modules/workers/require.js",
15 "resource://gre/modules/CanonicalJSON.jsm",
16 "resource://services-settings/IDBHelpers.jsm",
17 "resource://services-settings/SharedUtils.jsm",
18 "resource://gre/modules/third_party/jsesc/jsesc.js"
21 const IDB_RECORDS_STORE = "records";
22 const IDB_TIMESTAMPS_STORE = "timestamps";
24 let gShutdown = false;
28 * Return the canonical JSON serialization of the specified records.
29 * It has to match what is done on the server (See Kinto/kinto-signer).
31 * @param {Array<Object>} records
32 * @param {String} timestamp
35 async canonicalStringify(records, timestamp) {
36 // Sort list by record id.
37 let allRecords = records.sort((a, b) => {
41 return a.id > b.id ? 1 : 0;
43 // All existing records are replaced by the version from the server
44 // and deleted records are removed.
45 for (let i = 0; i < allRecords.length /* no increment! */; ) {
46 const rec = allRecords[i];
47 const next = allRecords[i + 1];
48 if ((next && rec.id == next.id) || rec.deleted) {
49 allRecords.splice(i, 1); // remove local record
55 last_modified: "" + timestamp,
58 return CanonicalJSON.stringify(toSerialize, jsesc);
62 * If present, import the JSON file into the Remote Settings IndexedDB
63 * for the specified bucket and collection.
64 * (eg. blocklists/certificates, main/onboarding)
65 * @param {String} bucket
66 * @param {String} collection
67 * @returns {int} Number of records loaded from dump or -1 if no dump found.
69 async importJSONDump(bucket, collection) {
70 const { data: records } = await SharedUtils.loadJSONDump(
74 if (records === null) {
75 // Return -1 if file is missing.
79 throw new Error("Can't import when we've started shutting down.");
81 await importDumpIDB(bucket, collection, records);
82 return records.length;
86 * Check that the specified file matches the expected size and SHA-256 hash.
87 * @param {String} fileUrl file URL to read from
88 * @param {Number} size expected file size
89 * @param {String} size expected file SHA-256 as hex string
92 async checkFileHash(fileUrl, size, hash) {
95 resp = await fetch(fileUrl);
97 // File does not exist.
100 const buffer = await resp.arrayBuffer();
101 return SharedUtils.checkContentHash(buffer, size, hash);
104 async prepareShutdown() {
106 // Ensure we can iterate and abort (which may delete items) by cloning
108 let transactions = Array.from(gPendingTransactions);
109 for (let transaction of transactions) {
113 // We can hit this case if the transaction has finished but
114 // we haven't heard about it yet.
119 _test_only_import(bucket, collection, records) {
120 return importDumpIDB(bucket, collection, records);
125 * Wrap worker invocations in order to return the `callbackId` along
126 * the result. This will allow to transform the worker invocations
127 * into promises in `RemoteSettingsWorker.jsm`.
129 self.onmessage = event => {
130 const { callbackId, method, args = [] } = event.data;
131 Agent[method](...args)
133 self.postMessage({ callbackId, result });
136 console.log(`RemoteSettingsWorker error: ${error}`);
137 self.postMessage({ callbackId, error: "" + error });
141 let gPendingTransactions = new Set();
144 * Import the records into the Remote Settings Chrome IndexedDB.
146 * Note: This duplicates some logics from `kinto-offline-client.js`.
148 * @param {String} bucket
149 * @param {String} collection
150 * @param {Array<Object>} records
152 async function importDumpIDB(bucket, collection, records) {
153 // Open the DB. It will exist since if we are running this, it means
154 // we already tried to read the timestamp in `remote-settings.js`
155 const db = await IDBHelpers.openIDB(false /* do not allow upgrades */);
157 // try...finally to ensure we always close the db.
160 throw new Error("Can't import when we've started shutting down.");
163 // Each entry of the dump will be stored in the records store.
164 // They are indexed by `_cid`.
165 const cid = bucket + "/" + collection;
166 // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump().
167 records.forEach(item => {
170 // Store the highest timestamp as the collection timestamp (or zero if dump is empty).
174 : Math.max(...records.map(record => record.last_modified));
175 let { transaction, promise } = IDBHelpers.executeIDB(
177 [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE],
179 ([recordsStore, timestampStore], rejectTransaction) => {
180 // Wipe before loading
181 recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true));
182 IDBHelpers.bulkOperationHelper(
185 reject: rejectTransaction,
187 timestampStore.put({ cid, value: timestamp });
195 gPendingTransactions.add(transaction);
196 promise = promise.finally(() => gPendingTransactions.delete(transaction));
199 // Close now that we're done.