summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ExtensionPermissions.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/extensions/ExtensionPermissions.jsm
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/ExtensionPermissions.jsm')
-rw-r--r--toolkit/components/extensions/ExtensionPermissions.jsm420
1 files changed, 420 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionPermissions.jsm b/toolkit/components/extensions/ExtensionPermissions.jsm
new file mode 100644
index 0000000000..bc56838ea7
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -0,0 +1,420 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* 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";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
+ JSONFile: "resource://gre/modules/JSONFile.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "StartupCache",
+ () => ExtensionParent.StartupCache
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "KeyValueService",
+ "resource://gre/modules/kvstore.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "Management",
+ () => ExtensionParent.apiManager
+);
+
+var EXPORTED_SYMBOLS = ["ExtensionPermissions"];
+
+// This is the old preference file pre-migration to rkv
+const FILE_NAME = "extension-preferences.json";
+
+function emptyPermissions() {
+ return { permissions: [], origins: [] };
+}
+
+const DEFAULT_VALUE = JSON.stringify(emptyPermissions());
+
+const VERSION_KEY = "_version";
+const VERSION_VALUE = 1;
+
+const KEY_PREFIX = "id-";
+
+// Bug 1646182: remove once we fully migrate to rkv
+let prefs;
+
+// Bug 1646182: remove once we fully migrate to rkv
+class LegacyPermissionStore {
+ async lazyInit() {
+ if (!this._initPromise) {
+ this._initPromise = this._init();
+ }
+ return this._initPromise;
+ }
+
+ async _init() {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_NAME);
+
+ prefs = new JSONFile({ path });
+ prefs.data = {};
+
+ try {
+ let { buffer } = await OS.File.read(path);
+ prefs.data = JSON.parse(new TextDecoder().decode(buffer));
+ } catch (e) {
+ if (!e.becauseNoSuchFile) {
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ async has(extensionId) {
+ await this.lazyInit();
+ return !!prefs.data[extensionId];
+ }
+
+ async get(extensionId) {
+ await this.lazyInit();
+
+ let perms = prefs.data[extensionId];
+ if (!perms) {
+ perms = emptyPermissions();
+ }
+
+ return perms;
+ }
+
+ async put(extensionId, permissions) {
+ await this.lazyInit();
+ prefs.data[extensionId] = permissions;
+ prefs.saveSoon();
+ }
+
+ async delete(extensionId) {
+ await this.lazyInit();
+ if (prefs.data[extensionId]) {
+ delete prefs.data[extensionId];
+ prefs.saveSoon();
+ }
+ }
+
+ async uninitForTest() {
+ if (!this._initPromise) {
+ return;
+ }
+
+ await this._initPromise;
+ await prefs.finalize();
+ prefs = null;
+ this._initPromise = null;
+ }
+
+ async resetVersionForTest() {
+ throw new Error("Not supported");
+ }
+}
+
+class PermissionStore {
+ async _init() {
+ const storePath = FileUtils.getDir("ProfD", ["extension-store"]).path;
+ // Make sure the folder exists
+ await OS.File.makeDir(storePath, { ignoreExisting: true });
+ this._store = await KeyValueService.getOrCreate(storePath, "permissions");
+ if (!(await this._store.has(VERSION_KEY))) {
+ await this.maybeMigrateData();
+ }
+ }
+
+ lazyInit() {
+ if (!this._initPromise) {
+ this._initPromise = this._init();
+ }
+ return this._initPromise;
+ }
+
+ validateMigratedData(json) {
+ let data = {};
+ for (let [extensionId, permissions] of Object.entries(json)) {
+ // If both arrays are empty there's no need to include the value since
+ // it's the default
+ if (
+ "permissions" in permissions &&
+ "origins" in permissions &&
+ (permissions.permissions.length || permissions.origins.length)
+ ) {
+ data[extensionId] = permissions;
+ }
+ }
+ return data;
+ }
+
+ async maybeMigrateData() {
+ let migrationWasSuccessful = false;
+ let oldStore = OS.Path.join(OS.Constants.Path.profileDir, FILE_NAME);
+ try {
+ await this.migrateFrom(oldStore);
+ migrationWasSuccessful = true;
+ } catch (e) {
+ if (!e.becauseNoSuchFile) {
+ Cu.reportError(e);
+ }
+ }
+
+ await this._store.put(VERSION_KEY, VERSION_VALUE);
+
+ if (migrationWasSuccessful) {
+ OS.File.remove(oldStore);
+ }
+ }
+
+ async migrateFrom(oldStore) {
+ // Some other migration job might have started and not completed, let's
+ // start from scratch
+ await this._store.clear();
+
+ let { buffer } = await OS.File.read(oldStore);
+ let json = JSON.parse(new TextDecoder().decode(buffer));
+ let data = this.validateMigratedData(json);
+
+ if (data) {
+ let entries = Object.entries(data).map(([extensionId, permissions]) => [
+ this.makeKey(extensionId),
+ JSON.stringify(permissions),
+ ]);
+ if (entries.length) {
+ await this._store.writeMany(entries);
+ }
+ }
+ }
+
+ makeKey(extensionId) {
+ // We do this so that the extensionId field cannot clash with internal
+ // fields like `_version`
+ return KEY_PREFIX + extensionId;
+ }
+
+ async has(extensionId) {
+ await this.lazyInit();
+ return this._store.has(this.makeKey(extensionId));
+ }
+
+ async get(extensionId) {
+ await this.lazyInit();
+ return this._store
+ .get(this.makeKey(extensionId), DEFAULT_VALUE)
+ .then(JSON.parse);
+ }
+
+ async put(extensionId, permissions) {
+ await this.lazyInit();
+ return this._store.put(
+ this.makeKey(extensionId),
+ JSON.stringify(permissions)
+ );
+ }
+
+ async delete(extensionId) {
+ await this.lazyInit();
+ return this._store.delete(this.makeKey(extensionId));
+ }
+
+ async resetVersionForTest() {
+ await this.lazyInit();
+ return this._store.delete(VERSION_KEY);
+ }
+
+ async uninitForTest() {
+ // Nothing special to do to unitialize, let's just
+ // make sure we're not in the middle of initialization
+ return this._initPromise;
+ }
+}
+
+// Bug 1646182: turn on rkv on all channels
+function createStore(useRkv = AppConstants.NIGHTLY_BUILD) {
+ if (useRkv) {
+ return new PermissionStore();
+ }
+ return new LegacyPermissionStore();
+}
+
+let store = createStore();
+
+var ExtensionPermissions = {
+ async _update(extensionId, perms) {
+ await store.put(extensionId, perms);
+ return StartupCache.permissions.set(extensionId, perms);
+ },
+
+ async _get(extensionId) {
+ return store.get(extensionId);
+ },
+
+ async _getCached(extensionId) {
+ return StartupCache.permissions.get(extensionId, () =>
+ this._get(extensionId)
+ );
+ },
+
+ /**
+ * Retrieves the optional permissions for the given extension.
+ * The information may be retrieved from the StartupCache, and otherwise fall
+ * back to data from the disk (and cache the result in the StartupCache).
+ *
+ * @param {string} extensionId The extensionId
+ * @returns {object} An object with "permissions" and "origins" array.
+ * The object may be a direct reference to the storage or cache, so its
+ * value should immediately be used and not be modified by callers.
+ */
+ get(extensionId) {
+ return this._getCached(extensionId);
+ },
+
+ _fixupAllUrlsPerms(perms) {
+ // Unfortunately, we treat <all_urls> as an API permission as well.
+ // If it is added to either, ensure it is added to both.
+ if (perms.origins.includes("<all_urls>")) {
+ perms.permissions.push("<all_urls>");
+ } else if (perms.permissions.includes("<all_urls>")) {
+ perms.origins.push("<all_urls>");
+ }
+ },
+
+ /**
+ * Add new permissions for the given extension. `permissions` is
+ * in the format that is passed to browser.permissions.request().
+ *
+ * @param {string} extensionId The extension id
+ * @param {Object} perms Object with permissions and origins array.
+ * @param {EventEmitter} emitter optional object implementing emitter interfaces
+ */
+ async add(extensionId, perms, emitter) {
+ let { permissions, origins } = await this._get(extensionId);
+
+ let added = emptyPermissions();
+
+ this._fixupAllUrlsPerms(perms);
+
+ for (let perm of perms.permissions) {
+ if (!permissions.includes(perm)) {
+ added.permissions.push(perm);
+ permissions.push(perm);
+ }
+ }
+
+ for (let origin of perms.origins) {
+ origin = new MatchPattern(origin, { ignorePath: true }).pattern;
+ if (!origins.includes(origin)) {
+ added.origins.push(origin);
+ origins.push(origin);
+ }
+ }
+
+ if (added.permissions.length || added.origins.length) {
+ await this._update(extensionId, { permissions, origins });
+ Management.emit("change-permissions", { extensionId, added });
+ if (emitter) {
+ emitter.emit("add-permissions", added);
+ }
+ }
+ },
+
+ /**
+ * Revoke permissions from the given extension. `permissions` is
+ * in the format that is passed to browser.permissions.request().
+ *
+ * @param {string} extensionId The extension id
+ * @param {Object} perms Object with permissions and origins array.
+ * @param {EventEmitter} emitter optional object implementing emitter interfaces
+ */
+ async remove(extensionId, perms, emitter) {
+ let { permissions, origins } = await this._get(extensionId);
+
+ let removed = emptyPermissions();
+
+ this._fixupAllUrlsPerms(perms);
+
+ for (let perm of perms.permissions) {
+ let i = permissions.indexOf(perm);
+ if (i >= 0) {
+ removed.permissions.push(perm);
+ permissions.splice(i, 1);
+ }
+ }
+
+ for (let origin of perms.origins) {
+ origin = new MatchPattern(origin, { ignorePath: true }).pattern;
+
+ let i = origins.indexOf(origin);
+ if (i >= 0) {
+ removed.origins.push(origin);
+ origins.splice(i, 1);
+ }
+ }
+
+ if (removed.permissions.length || removed.origins.length) {
+ await this._update(extensionId, { permissions, origins });
+ Management.emit("change-permissions", { extensionId, removed });
+ if (emitter) {
+ emitter.emit("remove-permissions", removed);
+ }
+ }
+ },
+
+ async removeAll(extensionId) {
+ StartupCache.permissions.delete(extensionId);
+
+ let removed = store.get(extensionId);
+ await store.delete(extensionId);
+ Management.emit("change-permissions", {
+ extensionId,
+ removed: await removed,
+ });
+ },
+
+ // This is meant for tests only
+ async _has(extensionId) {
+ return store.has(extensionId);
+ },
+
+ // This is meant for tests only
+ async _resetVersion() {
+ await store.resetVersionForTest();
+ },
+
+ // This is meant for tests only
+ _useLegacyStorageBackend: false,
+
+ // This is meant for tests only
+ async _uninit() {
+ await store.uninitForTest();
+ store = createStore(!this._useLegacyStorageBackend);
+ },
+
+ // Convenience listener members for all permission changes.
+ addListener(listener) {
+ Management.on("change-permissions", listener);
+ },
+
+ removeListener(listener) {
+ Management.off("change-permissions", listener);
+ },
+};