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/. */
"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;
ChromeUtils.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;
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"prefStorageSyncEnabled",
"webextensions.storage.sync.enabled",
true
);
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);
}
};
// Set of extensions that already recorded the sync quota event.
const syncQuotasRecorded = new WeakSet();
const QUOTA_BYTES_PER_ITEM = 8_192;
async function recordSyncQuotaTelemetry(extension, context) {
if (syncQuotasRecorded.has(extension) || !prefStorageSyncEnabled) {
return;
}
syncQuotasRecorded.add(extension);
let items = await extensionStorageSync.get(extension, null, context);
let items_over_quota = 0;
let total_size_bytes = 0;
let entries = Object.entries(items);
for (let [key, value] of entries) {
let bytes = JSON.stringify(value).length;
total_size_bytes += key.length + bytes;
if (bytes > QUOTA_BYTES_PER_ITEM) {
items_over_quota++;
}
}
Glean.extensionsData.syncUsageQuotas.record({
addon_id: extension.id,
total_size_bytes,
items_count: entries.length,
items_over_quota,
backend: extensionStorageSync.backend,
});
}
// 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.sys.mjs was not used.
// ExtensionStorageSync.sys.mjs 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 QUOTA_BYTES() {
return extensionStorageSession.QUOTA_BYTES;
},
get(items) {
return extensionStorageSession.get(extension, items);
},
set(items, { callerLocation } = {}) {
const { warningError } =
extensionStorageSession.set(extension, items) ?? {};
if (warningError) {
context.logConsoleScriptError({
message: warningError.message,
fileName: callerLocation?.source,
lineNumber: callerLocation?.line,
columnNumber: callerLocation?.column,
flags: Ci.nsIScriptError.warningFlag,
innerWindowID:
context.browsingContext.currentWindowContext.innerWindowId,
});
}
},
remove(keys) {
extensionStorageSession.remove(extension, keys);
},
clear() {
extensionStorageSession.clear(extension);
},
getBytesInUse(keys) {
return extensionStorageSession.getBytesInUse(extension, keys);
},
onChanged: new EventManager({
context,
module: "storage",
event: "session.onChanged",
extensionApi: this,
}).api(),
},
sync: {
get(spec) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.get(extension, spec, context);
},
set(items) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.set(extension, items, context);
},
remove(keys) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.remove(extension, keys, context);
},
clear() {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.clear(extension, context);
},
getBytesInUse(keys) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
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(),
},
};
}
};