Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
return console.createInstance({
maxLogLevelPref:
"privacy.trackingprotection.content.remote_settings.loglevel",
prefix: "ContentClassifier",
});
});
const COLLECTION_NAME = "content-classifier-lists";
/**
* RemoteSettings client for the content classifier. Owns the filter
* list bytes (via the RS attachments cache on disk), notifies the
* C++ ContentClassifierService of list updates and removals, and
* serves bytes back on demand via getListBytes().
*/
export class ContentClassifierRemoteSettingsClient {
classID = Components.ID("{C7DDDBF2-8BC4-41A1-AC90-5144BEC5ABDF}");
QueryInterface = ChromeUtils.generateQI([
"nsIContentClassifierRemoteSettingsClient",
]);
#rs = null;
#onSyncCallback = null;
#service = null;
#initialized = false;
constructor() {
this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
}
/**
* Initialize the client and register a sync
* listener for future updates. Resolves once the initial import has
* finished (or failed).
*
* The service keeps a strong reference to this client via mRSClient
* and we keep a strong reference back via #service. The resulting
* cycle is broken explicitly when the service calls shutdown().
*
* @param {nsIContentClassifierService} service
*/
async init(service) {
if (!service) {
throw new Error("Missing required argument service");
}
if (this.#initialized) {
lazy.log.debug(`init: already initialized`);
return;
}
this.#initialized = true;
this.#service = service;
lazy.log.debug(`init: starting import`);
// Register the sync listener before the async import so a sync event
// that fires during the import is not lost.
this.#onSyncCallback = this.onSync.bind(this);
this.#rs.on("sync", this.#onSyncCallback);
await this.importAllLists();
lazy.log.debug(`init: done, sync listener registered`);
}
/**
* Shut down the client and unregister the sync listener.
*/
shutdown() {
lazy.log.debug(`shutdown`);
if (this.#onSyncCallback) {
this.#rs.off("sync", this.#onSyncCallback);
this.#onSyncCallback = null;
}
this.#service = null;
this.#initialized = false;
}
/**
* Handle a RemoteSettings sync event. Notifies the service,
* which then pulls list bytes lazily via
* getListBytes when it actually rebuilds an engine.
*
* @param {object} event
* @param {object} event.data
* @param {Array} event.data.created Newly added records.
* @param {Array} event.data.updated Records with {old, new} pairs.
* @param {Array} event.data.deleted Removed records.
*/
async onSync({ data: { created = [], updated = [], deleted = [] } }) {
let service = this.#service;
if (!service) {
return;
}
lazy.log.debug(
`onSync: ${created.length} created, ${updated.length} updated, ${deleted.length} deleted`
);
for (let record of deleted) {
try {
await this.#rs.attachments.deleteDownloaded(record);
} catch (e) {
lazy.log.error(`Failed to delete attachment for "${record.Name}":`, e);
}
}
let updatedNames = [
...updated.map(({ new: r }) => r.Name),
...created.map(r => r.Name),
];
let removedNames = deleted.map(r => r.Name);
service.onListsChanged(updatedNames, removedNames);
}
/**
* Fetch all records from the collection and notify the service.
*/
async importAllLists() {
let service = this.#service;
if (!service) {
return;
}
let records = [];
try {
records = await this.#rs.get();
lazy.log.debug(`importAllLists: got ${records.length} records`);
} catch (error) {
lazy.log.error(`Error importing lists:`, error);
}
service.onListsChanged(
records.map(r => r.Name),
[]
);
}
/**
* Return a Uint8Array containing the bytes of the named list's
* attachment. Throws if no record with that name exists or the
* attachment cannot be obtained.
*
* @param {string} name The Name field of the RemoteSettings record.
* @returns {Promise<Uint8Array>}
*/
async getListBytes(name) {
let records = await this.#rs.get();
let record = records.find(r => r.Name === name);
if (!record) {
throw new Error(`No record with name "${name}"`);
}
if (!record.attachment) {
throw new Error(`Record "${name}" has no attachment`);
}
let result;
try {
result = await this.#rs.attachments.download(record, {
fallbackToCache: true,
fallbackToDump: true,
});
} catch (e) {
lazy.log.error(`Failed to download attachment for "${name}":`, e);
throw new Error(`Failed to download attachment for "${name}"`);
}
return new Uint8Array(result.buffer);
}
}