"use strict"; ChromeUtils.defineModuleGetter(this, "ExtensionStorage", "resource://gre/modules/ExtensionStorage.jsm"); ChromeUtils.defineModuleGetter(this, "ExtensionStorageIDB", "resource://gre/modules/ExtensionStorageIDB.jsm"); ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"); // Telemetry histogram keys for the JSONFile backend. const storageGetHistogram = "WEBEXT_STORAGE_LOCAL_GET_MS"; const storageSetHistogram = "WEBEXT_STORAGE_LOCAL_SET_MS"; // Telemetry histogram keys for the IndexedDB backend. const storageGetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_GET_MS"; const storageSetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_SET_MS"; // Wrap a storage operation in a TelemetryStopWatch. async function measureOp(histogram, fn) { const stopwatchKey = {}; TelemetryStopwatch.start(histogram, stopwatchKey); try { let result = await fn(); TelemetryStopwatch.finish(histogram, stopwatchKey); return result; } catch (err) { TelemetryStopwatch.cancel(histogram, stopwatchKey); throw err; } } this.storage = class extends ExtensionAPI { getLocalFileBackend(context, {deserialize, serialize}) { return { get(keys) { return measureOp(storageGetHistogram, () => { return context.childManager.callParentAsyncFunction( "storage.local.JSONFileBackend.get", [serialize(keys)]).then(deserialize); }); }, set(items) { return measureOp(storageSetHistogram, () => { return context.childManager.callParentAsyncFunction( "storage.local.JSONFileBackend.set", [serialize(items)]); }); }, remove(keys) { return context.childManager.callParentAsyncFunction( "storage.local.JSONFileBackend.remove", [serialize(keys)]); }, clear() { return context.childManager.callParentAsyncFunction( "storage.local.JSONFileBackend.clear", []); }, }; } getLocalIDBBackend(context, {hasParentListeners, serialize, storagePrincipal}) { const dbPromise = ExtensionStorageIDB.open(storagePrincipal); return { get(keys) { return measureOp(storageGetIDBHistogram, async () => { const db = await dbPromise; return db.get(keys); }); }, set(items) { return measureOp(storageSetIDBHistogram, async () => { const db = await dbPromise; const changes = await db.set(items, { serialize: ExtensionStorage.serialize, }); if (!changes) { return; } const hasListeners = await hasParentListeners(); if (hasListeners) { await context.childManager.callParentAsyncFunction( "storage.local.IDBBackend.fireOnChanged", [changes]); } }); }, async remove(keys) { const db = await dbPromise; const changes = await db.remove(keys); if (!changes) { return; } const hasListeners = await hasParentListeners(); if (hasListeners) { await context.childManager.callParentAsyncFunction( "storage.local.IDBBackend.fireOnChanged", [changes]); } }, async clear() { const db = await dbPromise; const changes = await db.clear(context.extension); if (!changes) { return; } const hasListeners = await hasParentListeners(); if (hasListeners) { await context.childManager.callParentAsyncFunction( "storage.local.IDBBackend.fireOnChanged", [changes]); } }, }; } getAPI(context) { const serialize = ExtensionStorage.serializeForContext.bind(null, context); const deserialize = ExtensionStorage.deserializeForContext.bind(null, context); function sanitize(items) { // The schema validator already takes care of arrays (which are only allowed // to contain strings). Strings and null are safe values. if (typeof items != "object" || items === null || Array.isArray(items)) { return items; } // If we got here, then `items` is an object generated by `ObjectType`'s // `normalize` method from Schemas.jsm. The object returned by `normalize` // lives in this compartment, while the values live in compartment of // `context.contentWindow`. The `sanitize` method runs with the principal // of `context`, so we cannot just use `ExtensionStorage.sanitize` because // it is not allowed to access properties of `items`. // So we enumerate all properties and sanitize each value individually. let sanitized = {}; for (let [key, value] of Object.entries(items)) { sanitized[key] = ExtensionStorage.sanitize(value, context); } return sanitized; } // Detect the actual storage.local enabled backend for the extension (as soon as the // storage.local API has been accessed for the first time). let promiseStorageLocalBackend; const getStorageLocalBackend = async () => { const { backendEnabled, storagePrincipal, } = await ExtensionStorageIDB.selectBackend(context); if (!backendEnabled) { return this.getLocalFileBackend(context, {deserialize, serialize}); } return this.getLocalIDBBackend(context, { storagePrincipal, hasParentListeners() { // We spare a good amount of memory if there are no listeners around // (e.g. because they have never been subscribed or they have been removed // in the meantime). return context.childManager.callParentAsyncFunction( "storage.local.IDBBackend.hasListeners", []); }, serialize, }); }; // Generate the backend-agnostic local API wrapped methods. const local = {}; for (let method of ["get", "set", "remove", "clear"]) { local[method] = async function(...args) { if (!promiseStorageLocalBackend) { promiseStorageLocalBackend = getStorageLocalBackend(); } const backend = await promiseStorageLocalBackend; return backend[method](...args); }; } return { storage: { local, sync: { get(keys) { keys = sanitize(keys); return context.childManager.callParentAsyncFunction("storage.sync.get", [ keys, ]); }, set(items) { items = sanitize(items); return context.childManager.callParentAsyncFunction("storage.sync.set", [ items, ]); }, }, managed: { get(keys) { return context.childManager.callParentAsyncFunction("storage.managed.get", [ serialize(keys), ]).then(deserialize); }, set(items) { return Promise.reject({message: "storage.managed is read-only"}); }, remove(keys) { return Promise.reject({message: "storage.managed is read-only"}); }, clear() { return Promise.reject({message: "storage.managed is read-only"}); }, }, onChanged: new EventManager({ context, name: "storage.onChanged", register: fire => { let onChanged = (data, area) => { let changes = new context.cloneScope.Object(); for (let [key, value] of Object.entries(data)) { changes[key] = deserialize(value); } fire.raw(changes, area); }; let parent = context.childManager.getParentEvent("storage.onChanged"); parent.addListener(onChanged); return () => { parent.removeListener(onChanged); }; }, }).api(), }, }; } };