diff options
Diffstat (limited to 'toolkit/components/extensions/parent/ext-storage.js')
-rw-r--r-- | toolkit/components/extensions/parent/ext-storage.js | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-storage.js b/toolkit/components/extensions/parent/ext-storage.js new file mode 100644 index 0000000000..2f043ce688 --- /dev/null +++ b/toolkit/components/extensions/parent/ext-storage.js @@ -0,0 +1,366 @@ +/* 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs", + ExtensionStorage: "resource://gre/modules/ExtensionStorage.sys.mjs", + ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.sys.mjs", + NativeManifests: "resource://gre/modules/NativeManifests.sys.mjs", + extensionStorageSession: "resource://gre/modules/ExtensionStorage.sys.mjs", +}); + +var { ExtensionError } = ExtensionUtils; +var { ignoreEvent } = ExtensionCommon; + +XPCOMUtils.defineLazyGetter(this, "extensionStorageSync", () => { + // TODO bug 1637465: Remove Kinto-based implementation. + if (Services.prefs.getBoolPref("webextensions.storage.sync.kinto")) { + const { extensionStorageSyncKinto } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionStorageSyncKinto.sys.mjs" + ); + return extensionStorageSyncKinto; + } + + const { extensionStorageSync } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionStorageSync.sys.mjs" + ); + return extensionStorageSync; +}); + +const enforceNoTemporaryAddon = extensionId => { + const EXCEPTION_MESSAGE = + "The storage API will not work with a temporary addon ID. " + + "Please add an explicit addon ID to your manifest. " + + "For more information see https://mzl.la/3lPk1aE."; + if (AddonManagerPrivate.isTemporaryInstallID(extensionId)) { + throw new ExtensionError(EXCEPTION_MESSAGE); + } +}; + +// WeakMap[extension -> Promise<SerializableMap?>] +const managedStorage = new WeakMap(); + +const lookupManagedStorage = async (extensionId, context) => { + if (Services.policies) { + let extensionPolicy = Services.policies.getExtensionPolicy(extensionId); + if (extensionPolicy) { + return ExtensionStorage._serializableMap(extensionPolicy); + } + } + let info = await NativeManifests.lookupManifest( + "storage", + extensionId, + context + ); + if (info) { + return ExtensionStorage._serializableMap(info.manifest.data); + } + return null; +}; + +this.storage = class extends ExtensionAPIPersistent { + constructor(extension) { + super(extension); + + const messageName = `Extension:StorageLocalOnChanged:${extension.uuid}`; + Services.ppmm.addMessageListener(messageName, this); + this.clearStorageChangedListener = () => { + Services.ppmm.removeMessageListener(messageName, this); + }; + } + + PERSISTENT_EVENTS = { + onChanged({ context, fire }) { + let unregisterLocal = this.registerLocalChangedListener(changes => { + // |changes| is already serialized. Send the raw value, so that it can + // be deserialized by the onChanged handler in child/ext-storage.js. + fire.raw(changes, "local"); + }); + + // Session storage is not exposed to content scripts, and `context` does + // not exist while setting up persistent listeners for an event page. + let unregisterSession; + if ( + !context || + context.envType === "addon_parent" || + context.envType === "devtools_parent" + ) { + unregisterSession = extensionStorageSession.registerListener( + this.extension, + changes => fire.async(changes, "session") + ); + } + + let unregisterSync = this.registerSyncChangedListener(changes => { + fire.async(changes, "sync"); + }); + + return { + unregister() { + unregisterLocal(); + unregisterSession?.(); + unregisterSync(); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + "local.onChanged"({ fire }) { + let unregister = this.registerLocalChangedListener(changes => { + // |changes| is already serialized. Send the raw value, so that it can + // be deserialized by the onChanged handler in child/ext-storage.js. + fire.raw(changes); + }); + return { + unregister, + convert(_fire) { + fire = _fire; + }, + }; + }, + "session.onChanged"({ fire }) { + let unregister = extensionStorageSession.registerListener( + this.extension, + changes => fire.async(changes) + ); + + return { + unregister, + convert(_fire) { + fire = _fire; + }, + }; + }, + "sync.onChanged"({ fire }) { + let unregister = this.registerSyncChangedListener(changes => { + fire.async(changes); + }); + return { + unregister, + convert(_fire) { + fire = _fire; + }, + }; + }, + }; + + registerLocalChangedListener(onStorageLocalChanged) { + const extensionId = this.extension.id; + ExtensionStorage.addOnChangedListener(extensionId, onStorageLocalChanged); + ExtensionStorageIDB.addOnChangedListener( + extensionId, + onStorageLocalChanged + ); + return () => { + ExtensionStorage.removeOnChangedListener( + extensionId, + onStorageLocalChanged + ); + ExtensionStorageIDB.removeOnChangedListener( + extensionId, + onStorageLocalChanged + ); + }; + } + + registerSyncChangedListener(onStorageSyncChanged) { + const { extension } = this; + let closeCallback; + // The ExtensionStorageSyncKinto implementation of addOnChangedListener + // relies on context.callOnClose (via ExtensionStorageSync.registerInUse) + // to keep track of active users of the storage. We don't need to pass a + // real BaseContext instance, a dummy object with the callOnClose method + // works too. This enables us to register a primed listener before any + // context is available. + // TODO bug 1637465: Remove this when the Kinto backend is dropped. + let dummyContextForKinto = { + callOnClose({ close }) { + closeCallback = close; + }, + }; + extensionStorageSync.addOnChangedListener( + extension, + onStorageSyncChanged, + dummyContextForKinto + ); + return () => { + extensionStorageSync.removeOnChangedListener( + extension, + onStorageSyncChanged + ); + // May be void if ExtensionStorageSyncKinto.jsm was not used. + // ExtensionStorageSync.jsm does not use the context. + closeCallback?.(); + }; + } + + onShutdown() { + const { clearStorageChangedListener } = this; + this.clearStorageChangedListener = null; + + if (clearStorageChangedListener) { + clearStorageChangedListener(); + } + } + + receiveMessage({ name, data }) { + if (name !== `Extension:StorageLocalOnChanged:${this.extension.uuid}`) { + return; + } + + ExtensionStorageIDB.notifyListeners(this.extension.id, data); + } + + getAPI(context) { + let { extension } = context; + + return { + storage: { + local: { + async callMethodInParentProcess(method, args) { + const res = await ExtensionStorageIDB.selectBackend({ extension }); + if (!res.backendEnabled) { + return ExtensionStorage[method](extension.id, ...args); + } + + const persisted = extension.hasPermission("unlimitedStorage"); + const db = await ExtensionStorageIDB.open( + res.storagePrincipal.deserialize(this, true), + persisted + ); + try { + const changes = await db[method](...args); + if (changes) { + ExtensionStorageIDB.notifyListeners(extension.id, changes); + } + return changes; + } catch (err) { + const normalizedError = ExtensionStorageIDB.normalizeStorageError( + { + error: err, + extensionId: extension.id, + storageMethod: method, + } + ).message; + return Promise.reject({ + message: String(normalizedError), + }); + } + }, + // Private storage.local JSONFile backend methods (used internally by the child + // ext-storage.js module). + JSONFileBackend: { + get(spec) { + return ExtensionStorage.get(extension.id, spec); + }, + set(items) { + return ExtensionStorage.set(extension.id, items); + }, + remove(keys) { + return ExtensionStorage.remove(extension.id, keys); + }, + clear() { + return ExtensionStorage.clear(extension.id); + }, + }, + // Private storage.local IDB backend methods (used internally by the child ext-storage.js + // module). + IDBBackend: { + selectBackend() { + return ExtensionStorageIDB.selectBackend(context); + }, + }, + onChanged: new EventManager({ + context, + module: "storage", + event: "local.onChanged", + extensionApi: this, + }).api(), + }, + + session: { + get(items) { + return extensionStorageSession.get(extension, items); + }, + set(items) { + extensionStorageSession.set(extension, items); + }, + remove(keys) { + extensionStorageSession.remove(extension, keys); + }, + clear() { + extensionStorageSession.clear(extension); + }, + onChanged: new EventManager({ + context, + module: "storage", + event: "session.onChanged", + extensionApi: this, + }).api(), + }, + + sync: { + get(spec) { + enforceNoTemporaryAddon(extension.id); + return extensionStorageSync.get(extension, spec, context); + }, + set(items) { + enforceNoTemporaryAddon(extension.id); + return extensionStorageSync.set(extension, items, context); + }, + remove(keys) { + enforceNoTemporaryAddon(extension.id); + return extensionStorageSync.remove(extension, keys, context); + }, + clear() { + enforceNoTemporaryAddon(extension.id); + return extensionStorageSync.clear(extension, context); + }, + getBytesInUse(keys) { + enforceNoTemporaryAddon(extension.id); + return extensionStorageSync.getBytesInUse(extension, keys, context); + }, + onChanged: new EventManager({ + context, + module: "storage", + event: "sync.onChanged", + extensionApi: this, + }).api(), + }, + + managed: { + async get(keys) { + enforceNoTemporaryAddon(extension.id); + let lookup = managedStorage.get(extension); + + if (!lookup) { + lookup = lookupManagedStorage(extension.id, context); + managedStorage.set(extension, lookup); + } + + let data = await lookup; + if (!data) { + return Promise.reject({ + message: "Managed storage manifest not found", + }); + } + return ExtensionStorage._filterProperties(extension.id, data, keys); + }, + // managed storage is currently initialized once. + onChanged: ignoreEvent(context, "storage.managed.onChanged"), + }, + + onChanged: new EventManager({ + context, + module: "storage", + event: "onChanged", + extensionApi: this, + }).api(), + }, + }; + } +}; |