mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-08 12:19:05 +02:00
Depends on D149707 Differential Revision: https://phabricator.services.mozilla.com/D149708
314 lines
9.8 KiB
JavaScript
314 lines
9.8 KiB
JavaScript
/* 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";
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
|
|
ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
|
|
ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
|
|
NativeManifests: "resource://gre/modules/NativeManifests.jsm",
|
|
});
|
|
|
|
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.import(
|
|
"resource://gre/modules/ExtensionStorageSyncKinto.jsm"
|
|
);
|
|
return extensionStorageSyncKinto;
|
|
}
|
|
|
|
const { extensionStorageSync } = ChromeUtils.import(
|
|
"resource://gre/modules/ExtensionStorageSync.jsm"
|
|
);
|
|
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({ 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");
|
|
});
|
|
let unregisterSync = this.registerSyncChangedListener(changes => {
|
|
fire.async(changes, "sync");
|
|
});
|
|
return {
|
|
unregister() {
|
|
unregisterLocal();
|
|
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;
|
|
},
|
|
};
|
|
},
|
|
"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(),
|
|
},
|
|
|
|
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(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(),
|
|
},
|
|
};
|
|
}
|
|
};
|