mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-01 00:38:50 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1018 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1018 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| import { ExtensionTaskScheduler } from "resource://gre/modules/ExtensionTaskScheduler.sys.mjs";
 | |
| import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | |
| 
 | |
| const lazy = XPCOMUtils.declareLazy({
 | |
|   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
 | |
|   AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
 | |
|   Extension: "resource://gre/modules/Extension.sys.mjs",
 | |
|   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
 | |
|   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
 | |
|   JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
 | |
|   KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
 | |
|   StartupCache: "resource://gre/modules/ExtensionParent.sys.mjs",
 | |
|   Management: () => lazy.ExtensionParent.apiManager,
 | |
| });
 | |
| 
 | |
| function emptyPermissions() {
 | |
|   return { permissions: [], origins: [], data_collection: [] };
 | |
| }
 | |
| 
 | |
| const DEFAULT_VALUE = JSON.stringify(emptyPermissions());
 | |
| 
 | |
| const KEY_PREFIX = "id-";
 | |
| 
 | |
| // This is the old preference file pre-migration to rkv.
 | |
| const OLD_JSON_FILENAME = "extension-preferences.json";
 | |
| // This is the old path to the rkv store dir (which used to be shared with ExtensionScriptingStore).
 | |
| const OLD_RKV_DIRNAME = "extension-store";
 | |
| // This is the new path to the rkv store dir.
 | |
| const RKV_DIRNAME = "extension-store-permissions";
 | |
| 
 | |
| const VERSION_KEY = "_version";
 | |
| 
 | |
| const VERSION_VALUE = 1;
 | |
| 
 | |
| const WEB_SCHEMES = ["http", "https"];
 | |
| 
 | |
| // 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 = PathUtils.join(
 | |
|       Services.dirsvc.get("ProfD", Ci.nsIFile).path,
 | |
|       OLD_JSON_FILENAME
 | |
|     );
 | |
| 
 | |
|     prefs = new lazy.JSONFile({ path });
 | |
|     prefs.data = {};
 | |
| 
 | |
|     try {
 | |
|       prefs.data = await IOUtils.readJSON(path);
 | |
|     } catch (e) {
 | |
|       if (!(DOMException.isInstance(e) && e.name == "NotFoundError")) {
 | |
|         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 {
 | |
|   _shouldMigrateFromOldKVStorePath = AppConstants.NIGHTLY_BUILD;
 | |
| 
 | |
|   async _init() {
 | |
|     const storePath = lazy.FileUtils.getDir("ProfD", [RKV_DIRNAME]).path;
 | |
|     // Make sure the folder exists
 | |
|     await IOUtils.makeDirectory(storePath, { ignoreExisting: true });
 | |
|     this._store = await lazy.KeyValueService.getOrCreateWithOptions(
 | |
|       storePath,
 | |
|       "permissions",
 | |
|       { strategy: lazy.KeyValueService.RecoveryStrategy.RENAME }
 | |
|     );
 | |
|     if (!(await this._store.has(VERSION_KEY))) {
 | |
|       // If _shouldMigrateFromOldKVStorePath is true (default only on Nightly channel
 | |
|       // where the rkv store has been enabled by default for a while), we need to check
 | |
|       // if we would need to import data from the old kvstore path (ProfD/extensions-store)
 | |
|       // first, and fallback to try to import from the JSONFile if there was no data in
 | |
|       // the old kvstore path.
 | |
|       // NOTE: _shouldMigrateFromOldKVStorePath is also explicitly set to true in unit tests
 | |
|       // that are meant to explicitly cover this path also when running on on non-Nightly channels.
 | |
|       if (this._shouldMigrateFromOldKVStorePath) {
 | |
|         // Try to import data from the old kvstore path (ProfD/extensions-store).
 | |
|         await this.maybeImportFromOldKVStorePath();
 | |
|         if (!(await this._store.has(VERSION_KEY))) {
 | |
|           // There was no data in the old kvstore path, migrate any data
 | |
|           // available from the LegacyPermissionStore JSONFile if any.
 | |
|           await this.maybeMigrateDataFromOldJSONFile();
 | |
|         }
 | |
|       } else {
 | |
|         // On non-Nightly channels, where LegacyPermissionStore was still the
 | |
|         // only backend ever enabled, try to import permissions data from the
 | |
|         // legacy JSONFile, if any data is available there.
 | |
|         await this.maybeMigrateDataFromOldJSONFile();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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 maybeMigrateDataFromOldJSONFile() {
 | |
|     let migrationWasSuccessful = false;
 | |
|     let oldStore = PathUtils.join(
 | |
|       Services.dirsvc.get("ProfD", Ci.nsIFile).path,
 | |
|       OLD_JSON_FILENAME
 | |
|     );
 | |
|     try {
 | |
|       await this.migrateFrom(oldStore);
 | |
|       migrationWasSuccessful = true;
 | |
|     } catch (e) {
 | |
|       if (!(DOMException.isInstance(e) && e.name == "NotFoundError")) {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     await this._store.put(VERSION_KEY, VERSION_VALUE);
 | |
| 
 | |
|     if (migrationWasSuccessful) {
 | |
|       IOUtils.remove(oldStore);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async maybeImportFromOldKVStorePath() {
 | |
|     try {
 | |
|       const oldStorePath = lazy.FileUtils.getDir("ProfD", [
 | |
|         OLD_RKV_DIRNAME,
 | |
|       ]).path;
 | |
|       if (!(await IOUtils.exists(oldStorePath))) {
 | |
|         return;
 | |
|       }
 | |
|       const oldStore = await lazy.KeyValueService.getOrCreate(
 | |
|         oldStorePath,
 | |
|         "permissions"
 | |
|       );
 | |
|       const enumerator = await oldStore.enumerate();
 | |
|       const kvpairs = [];
 | |
|       while (enumerator.hasMoreElements()) {
 | |
|         const { key, value } = enumerator.getNext();
 | |
|         kvpairs.push([key, value]);
 | |
|       }
 | |
| 
 | |
|       // NOTE: we don't add a VERSION_KEY entry explicitly here because
 | |
|       // if the database was not empty the VERSION_KEY is already set to
 | |
|       // 1 and will be copied into the new file as part of the pairs
 | |
|       // written below (along with the entries for the actual extensions
 | |
|       // permissions).
 | |
|       if (kvpairs.length) {
 | |
|         await this._store.writeMany(kvpairs);
 | |
|       }
 | |
| 
 | |
|       // NOTE: the old rkv store path used to be shared with the
 | |
|       // ExtensionScriptingStore, and so we are not removing the old
 | |
|       // rkv store dir here (that is going to be left to a separate
 | |
|       // migration we will be adding to ExtensionScriptingStore).
 | |
|     } catch (err) {
 | |
|       Cu.reportError(err);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async migrateFrom(oldStore) {
 | |
|     // Some other migration job might have started and not completed, let's
 | |
|     // start from scratch
 | |
|     await this._store.clear();
 | |
| 
 | |
|     let json = await IOUtils.readJSON(oldStore);
 | |
|     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();
 | |
| 
 | |
| // The public ExtensionPermissions.add, get, remove, removeAll methods may
 | |
| // interact with the same underlying data source. These methods are not
 | |
| // designed with concurrent modifications in mind, and therefore we
 | |
| // explicitly synchronize each operation, by processing them sequentially.
 | |
| const extPermAccessQueues = new ExtensionTaskScheduler();
 | |
| 
 | |
| export var ExtensionPermissions = {
 | |
|   /**
 | |
|    * A per-extension container for origins requested at runtime, not in the
 | |
|    * manifest. This is only preserved in memory for UI consistency.
 | |
|    *
 | |
|    * @type {Map<string, Set>}
 | |
|    */
 | |
|   tempOrigins: new ExtensionUtils.DefaultMap(() => new Set()),
 | |
| 
 | |
|   async _update(extensionId, perms) {
 | |
|     await store.put(extensionId, perms);
 | |
|     return lazy.StartupCache.permissions.set(extensionId, perms);
 | |
|   },
 | |
| 
 | |
|   async _get(extensionId) {
 | |
|     return store.get(extensionId);
 | |
|   },
 | |
| 
 | |
|   async _getCached(extensionId) {
 | |
|     return lazy.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 {Promise<object>} Object with "permissions" and "origins" arrays.
 | |
|    *   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 extPermAccessQueues.runReadTask(extensionId, () =>
 | |
|       this._getCached(extensionId).then(perms =>
 | |
|         this._fixupDataCollection(perms)
 | |
|       )
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Validate and normalize passed in `perms`, including a fixup to
 | |
|    * include all possible "all sites" permissions when appropriate.
 | |
|    *
 | |
|    * @throws if an origin or permission is not part of optional permissions.
 | |
|    *
 | |
|    * @typedef {object} Perms
 | |
|    * @property {string[]} origins
 | |
|    * @property {string[]} permissions
 | |
|    * @property {string[]} [data_collection]
 | |
|    *
 | |
|    * @param {Perms} perms api permissions and origins to be added/removed.
 | |
|    * @param {Perms} optional permissions and origins from the manifest.
 | |
|    * @returns {Perms} normalized
 | |
|    */
 | |
|   normalizeOptional(perms, optional) {
 | |
|     let allSites = false;
 | |
|     let patterns = new MatchPatternSet(optional.origins, { ignorePath: true });
 | |
|     let normalized = Object.assign({}, perms);
 | |
| 
 | |
|     for (let o of perms.origins) {
 | |
|       if (!patterns.subsumes(new MatchPattern(o))) {
 | |
|         throw new Error(`${o} was not declared in the manifest`);
 | |
|       }
 | |
|       // If this is one of the "all sites" permissions
 | |
|       allSites ||= lazy.Extension.isAllSitesPermission(o);
 | |
|     }
 | |
| 
 | |
|     if (allSites) {
 | |
|       // Grant/revoke ALL "all sites" optional permissions from the manifest.
 | |
|       let origins = perms.origins.concat(
 | |
|         optional.origins.filter(o => lazy.Extension.isAllSitesPermission(o))
 | |
|       );
 | |
|       normalized.origins = Array.from(new Set(origins));
 | |
|     }
 | |
| 
 | |
|     for (let p of perms.permissions) {
 | |
|       if (!optional.permissions.includes(p)) {
 | |
|         throw new Error(`${p} was not declared in optional_permissions`);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return normalized;
 | |
|   },
 | |
| 
 | |
|   _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>");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _fixupDataCollection(perms) {
 | |
|     if (!perms.data_collection) {
 | |
|       perms.data_collection = [];
 | |
|     }
 | |
|     return perms;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add new permissions for the given extension.  `permissions` is
 | |
|    * in the format that is passed to browser.permissions.request().
 | |
|    *
 | |
|    * @typedef {import("ExtensionCommon.sys.mjs").EventEmitter} EventEmitter
 | |
|    *
 | |
|    * @param {string} extensionId The extension id
 | |
|    * @param {Perms} perms Object with permissions and origins array.
 | |
|    * @param {EventEmitter} [emitter] optional object implementing emitter interfaces
 | |
|    */
 | |
|   async add(extensionId, perms, emitter) {
 | |
|     return extPermAccessQueues.runWriteTask(extensionId, async () => {
 | |
|       let {
 | |
|         permissions,
 | |
|         origins,
 | |
|         data_collection = [],
 | |
|       } = await this._get(extensionId);
 | |
| 
 | |
|       let added = emptyPermissions();
 | |
| 
 | |
|       this._fixupAllUrlsPerms(perms);
 | |
|       this._fixupDataCollection(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);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let perm of perms.data_collection) {
 | |
|         if (!data_collection.includes(perm)) {
 | |
|           added.data_collection.push(perm);
 | |
|           data_collection.push(perm);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         added.permissions.length ||
 | |
|         added.origins.length ||
 | |
|         added.data_collection.length
 | |
|       ) {
 | |
|         await this._update(extensionId, {
 | |
|           permissions,
 | |
|           origins,
 | |
|           data_collection,
 | |
|         });
 | |
|         lazy.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 {Perms} perms Object with permissions and origins array.
 | |
|    * @param {EventEmitter} [emitter] optional object implementing emitter interfaces
 | |
|    */
 | |
|   async remove(extensionId, perms, emitter) {
 | |
|     return extPermAccessQueues.runWriteTask(extensionId, async () => {
 | |
|       let {
 | |
|         permissions,
 | |
|         origins,
 | |
|         data_collection = [],
 | |
|       } = await this._get(extensionId);
 | |
| 
 | |
|       let removed = emptyPermissions();
 | |
| 
 | |
|       this._fixupAllUrlsPerms(perms);
 | |
|       this._fixupDataCollection(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);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let perm of perms.data_collection) {
 | |
|         let i = data_collection.indexOf(perm);
 | |
|         if (i >= 0) {
 | |
|           removed.data_collection.push(perm);
 | |
|           data_collection.splice(i, 1);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         removed.permissions.length ||
 | |
|         removed.origins.length ||
 | |
|         removed.data_collection.length
 | |
|       ) {
 | |
|         await this._update(extensionId, {
 | |
|           permissions,
 | |
|           origins,
 | |
|           data_collection,
 | |
|         });
 | |
|         lazy.Management.emit("change-permissions", { extensionId, removed });
 | |
|         if (emitter) {
 | |
|           emitter.emit("remove-permissions", removed);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       let temp = this.tempOrigins.get(extensionId);
 | |
|       for (let origin of removed.origins) {
 | |
|         temp.add(origin);
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   async removeAll(extensionId) {
 | |
|     return extPermAccessQueues.runWriteTask(extensionId, async () => {
 | |
|       this.tempOrigins.delete(extensionId);
 | |
|       lazy.StartupCache.permissions.delete(extensionId);
 | |
| 
 | |
|       let removed = store.get(extensionId);
 | |
|       await store.delete(extensionId);
 | |
|       lazy.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({ recreateStore = true } = {}) {
 | |
|     await store?.uninitForTest();
 | |
|     store = null;
 | |
|     if (recreateStore) {
 | |
|       store = createStore(!this._useLegacyStorageBackend);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // This is meant for tests only
 | |
|   _getStore() {
 | |
|     return store;
 | |
|   },
 | |
| 
 | |
|   // Convenience listener members for all permission changes.
 | |
|   addListener(listener) {
 | |
|     lazy.Management.on("change-permissions", listener);
 | |
|   },
 | |
| 
 | |
|   removeListener(listener) {
 | |
|     lazy.Management.off("change-permissions", listener);
 | |
|   },
 | |
| };
 | |
| 
 | |
| export var OriginControls = {
 | |
|   /**
 | |
|    * @typedef {object} NativeTab
 | |
|    * @property {XULBrowserElement} linkedBrowser
 | |
|    */
 | |
| 
 | |
|   /**
 | |
|    * Determine if the given Manifest V3 extension has a host permissions for
 | |
|    * the given tab which was one expected to be granted at install time (by
 | |
|    * being listed in host_permissions or derived from match patterns for
 | |
|    * content scripts declared in the manifest).
 | |
|    *
 | |
|    * NOTE: this helper method is only used for additional checks only hit for
 | |
|    * MV3 extensions, but the implementation is technically not strictly MV3
 | |
|    * specific.
 | |
|    *
 | |
|    * @param {WebExtensionPolicy} policy
 | |
|    * @param {NativeTab} nativeTab
 | |
|    * @returns {boolean} Whether the extension has a non optional host
 | |
|    * permission for the given tab.
 | |
|    */
 | |
|   hasMV3RequestedOrigin(policy, nativeTab) {
 | |
|     const uri = nativeTab.linkedBrowser?.currentURI;
 | |
| 
 | |
|     if (!uri) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // Determine if that are host permissions that would have been granted
 | |
|     // as install time that are matching the tab URI.
 | |
|     const manifestOrigins =
 | |
|       policy.extension.getManifestOriginsMatchPatternSet();
 | |
|     return manifestOrigins.matches(uri);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * @typedef {object} OriginControlState
 | |
|    * @param {boolean} noAccess        no options, can never access host.
 | |
|    * @param {boolean} whenClicked     option to access host when clicked.
 | |
|    * @param {boolean} alwaysOn        option to always access this host.
 | |
|    * @param {boolean} allDomains      option to access to all domains.
 | |
|    * @param {boolean} hasAccess       extension currently has access to host.
 | |
|    * @param {boolean} temporaryAccess extension has temporary access to the tab.
 | |
|    */
 | |
| 
 | |
|   /**
 | |
|    * Get origin controls state for a given extension on a given tab.
 | |
|    *
 | |
|    * @param {WebExtensionPolicy} policy
 | |
|    * @param {NativeTab} nativeTab
 | |
|    * @returns {OriginControlState} Extension origin controls for this host include:
 | |
|    */
 | |
|   getState(policy, nativeTab) {
 | |
|     // Note: don't use the nativeTab directly because it's different on mobile.
 | |
|     let tab = policy?.extension?.tabManager?.getWrapper(nativeTab);
 | |
|     let tabHasActiveTabPermission = tab?.hasActiveTabPermission;
 | |
|     let uri = tab?.browser.currentURI;
 | |
|     return this._getStateInternal(policy, { uri, tabHasActiveTabPermission });
 | |
|   },
 | |
| 
 | |
|   _getStateInternal(policy, { uri, tabHasActiveTabPermission }) {
 | |
|     let temporaryAccess = tabHasActiveTabPermission;
 | |
| 
 | |
|     if (!uri) {
 | |
|       return { noAccess: true };
 | |
|     }
 | |
| 
 | |
|     // activeTab and the resulting whenClicked state is only applicable for MV2
 | |
|     // extensions with a browser action and MV3 extensions (with or without).
 | |
|     let activeTab =
 | |
|       policy.permissions.includes("activeTab") &&
 | |
|       (policy.manifestVersion >= 3 || policy.extension?.hasBrowserActionUI);
 | |
| 
 | |
|     let couldRequest = policy.extension.optionalOrigins.matches(uri);
 | |
|     let hasAccess = policy.canAccessURI(uri);
 | |
| 
 | |
|     // If any of (MV2) content script patterns match the URI.
 | |
|     let csPatternMatches = false;
 | |
|     let quarantinedFrom = policy.quarantinedFromURI(uri);
 | |
| 
 | |
|     if (policy.manifestVersion < 3 && !hasAccess) {
 | |
|       csPatternMatches = policy.contentScripts.some(cs =>
 | |
|         cs.matches.patterns.some(p => p.matches(uri))
 | |
|       );
 | |
|       // MV2 access through content scripts is implicit.
 | |
|       hasAccess = csPatternMatches && !quarantinedFrom;
 | |
|     }
 | |
| 
 | |
|     // If extension is quarantined from this host, but could otherwise have
 | |
|     // access (via activeTab, optional, allowedOrigins or content scripts).
 | |
|     let quarantined =
 | |
|       quarantinedFrom &&
 | |
|       (activeTab ||
 | |
|         couldRequest ||
 | |
|         csPatternMatches ||
 | |
|         policy.allowedOrigins.matches(uri));
 | |
| 
 | |
|     if (
 | |
|       quarantined ||
 | |
|       (uri.scheme !== "https" && uri.scheme !== "http") ||
 | |
|       WebExtensionPolicy.isRestrictedURI(uri) ||
 | |
|       (!couldRequest && !hasAccess && !activeTab)
 | |
|     ) {
 | |
|       return { noAccess: true, quarantined };
 | |
|     }
 | |
| 
 | |
|     if (!couldRequest && !hasAccess && activeTab) {
 | |
|       return { whenClicked: true, temporaryAccess };
 | |
|     }
 | |
|     if (policy.allowedOrigins.matchesAllWebUrls) {
 | |
|       return { allDomains: true, hasAccess };
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       whenClicked: true,
 | |
|       alwaysOn: true,
 | |
|       temporaryAccess,
 | |
|       hasAccess,
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Whether to show the attention indicator for extension on current tab. We
 | |
|    * usually show attention when:
 | |
|    *
 | |
|    * - some permissions are needed (in MV3+)
 | |
|    * - the extension is not allowed on the domain (quarantined)
 | |
|    *
 | |
|    * @param {WebExtensionPolicy} policy an extension's policy
 | |
|    * @param {Window} window The window for which we can get the attention state
 | |
|    * @returns {{attention: boolean, quarantined: boolean}}
 | |
|    */
 | |
|   getAttentionState(policy, window) {
 | |
|     if (policy?.manifestVersion >= 3) {
 | |
|       const { selectedTab } = window.gBrowser;
 | |
|       const state = this.getState(policy, selectedTab);
 | |
|       // Request attention when the extension cannot access the current tab,
 | |
|       // but has a host permission that could be granted.
 | |
|       // Quarantined is always false when the feature is disabled.
 | |
|       const quarantined = !!state.quarantined;
 | |
|       let attention =
 | |
|         quarantined ||
 | |
|         (!!state.alwaysOn &&
 | |
|           !state.hasAccess &&
 | |
|           !state.temporaryAccess &&
 | |
|           this.hasMV3RequestedOrigin(policy, selectedTab));
 | |
| 
 | |
|       return { attention, quarantined };
 | |
|     }
 | |
| 
 | |
|     // No need to check whether the Quarantined Domains feature is enabled
 | |
|     // here, it's already done in `getState()`.
 | |
|     const state = this.getState(policy, window.gBrowser.selectedTab);
 | |
|     const attention = !!state.quarantined;
 | |
|     // If it needs attention, it's because of the quarantined domains.
 | |
|     return { attention, quarantined: attention };
 | |
|   },
 | |
| 
 | |
|   // Grant extension host permission to always run on this host.
 | |
|   setAlwaysOn(policy, uri) {
 | |
|     if (!policy.active) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Already granted.
 | |
|     if (policy.allowedOrigins.matches(uri)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Only try to compute the per-host host permissions on web scheme urls (http/https).
 | |
|     if (!WEB_SCHEMES.includes(uri.scheme)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Determine which one from the 3 set of granted host permissions
 | |
|     // (granting access to the given url's host and scheme) are subsumed
 | |
|     // by the optional host permissions declared by the extension.
 | |
|     let originPatterns = [];
 | |
|     const originPatternsChoices = [
 | |
|       // Single wildcard scheme permission for the current host.
 | |
|       [`*://${uri.host}/*`],
 | |
|       // Two separate scheme-specific permission for the current host.
 | |
|       WEB_SCHEMES.map(scheme => `${scheme}://${uri.host}/*`),
 | |
|       // One scheme-specific permission for the current host and scheme.
 | |
|       [`${uri.scheme}://${uri.host}/*`],
 | |
|     ];
 | |
|     for (const originPatternsChoice of originPatternsChoices) {
 | |
|       const choiceMatchPatternSet = new MatchPatternSet(originPatternsChoice);
 | |
|       const choiceSubsumed = choiceMatchPatternSet.patterns.every(mp =>
 | |
|         policy.extension.optionalOrigins.subsumes(mp)
 | |
|       );
 | |
|       if (choiceSubsumed) {
 | |
|         originPatterns = originPatternsChoice;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Nothing to grant.
 | |
|     if (!originPatterns.length) {
 | |
|       // This shouldn't be ever hit outside of unit tests and so we log an error
 | |
|       // to prevent it from being silently hit (and make it easier to investigate
 | |
|       // potential bugs in our OriginControls.getState logic that could leave to
 | |
|       // this).
 | |
|       Cu.reportError(
 | |
|         `Unxpected no host permission patterns to grant found for ${policy.debugName} on ${uri.spec}`
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let perms = { permissions: [], origins: originPatterns };
 | |
|     return ExtensionPermissions.add(policy.id, perms, policy.extension);
 | |
|   },
 | |
| 
 | |
|   // Revoke permission, extension should run only when clicked on this host.
 | |
|   setWhenClicked(policy, uri) {
 | |
|     if (!policy.active) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Return earlier if the extension doesn't really have access to the
 | |
|     // given url.
 | |
|     if (!policy.allowedOrigins.matches(uri)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Only try to revoke per-host host permissions on web scheme urls (http/https).
 | |
|     if (!WEB_SCHEMES.includes(uri.scheme)) {
 | |
|       // TODO: once we have introduce a user-controlled opt-in for file urls
 | |
|       // we could consider to remove that internal permission to revoke
 | |
|       // to the extension access to file urls (and the user would be able
 | |
|       // to grant it back from the addon manager).
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // NOTE: all urls wouldn't be currently be revoked and so in that case
 | |
|     // setWhenClicked is going to be a no-op.
 | |
|     const matchHost = new MatchPattern(`*://${uri.host}/*`);
 | |
|     const patternsToRevoke = policy.allowedOrigins.patterns
 | |
|       .filter(mp => mp.overlaps(matchHost))
 | |
|       .map(mp => mp.pattern)
 | |
|       .filter(pattern => !lazy.Extension.isAllSitesPermission(pattern));
 | |
| 
 | |
|     // Nothing to revoke.
 | |
|     if (!patternsToRevoke.length) {
 | |
|       // This shouldn't be ever hit outside of unit tests and so we log an error
 | |
|       // to prevent it from being silently hit (and make it easier to investigate
 | |
|       // potential bugs in our OriginControls.getState logic that could leave to
 | |
|       // this).
 | |
|       Cu.reportError(
 | |
|         `Unxpected no host permission patterns to revoke found for ${policy.debugName} on ${uri.spec}`
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let perms = {
 | |
|       permissions: [],
 | |
|       origins: patternsToRevoke,
 | |
|     };
 | |
|     return ExtensionPermissions.remove(policy.id, perms, policy.extension);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * @typedef {object} FluentIdInfo
 | |
|    * @param {string} default      the message ID corresponding to the state
 | |
|    *                              that should be displayed by default.
 | |
|    * @param {string | null} onHover an optional message ID to be shown when
 | |
|    *                              users hover interactive elements (e.g. a
 | |
|    *                              button).
 | |
|    */
 | |
| 
 | |
|   /**
 | |
|    * Get origin controls messages (fluent IDs) to be shown to users for a given
 | |
|    * extension on a given host. The messages might be different for extensions
 | |
|    * with a browser action (that might or might not open a popup).
 | |
|    *
 | |
|    * @param {object} params
 | |
|    * @param {WebExtensionPolicy} params.policy an extension's policy
 | |
|    * @param {NativeTab} params.tab             the current tab
 | |
|    * @param {boolean} params.isAction          this should be true for
 | |
|    *                                           extensions with a browser
 | |
|    *                                           action, false otherwise.
 | |
|    * @param {boolean} params.hasPopup          this should be true when the
 | |
|    *                                           browser action opens a popup,
 | |
|    *                                           false otherwise.
 | |
|    *
 | |
|    * @returns {FluentIdInfo?} An object with origin controls message IDs or
 | |
|    *                        `null` when there is no message for the state.
 | |
|    */
 | |
|   getStateMessageIDs({ policy, tab, isAction = false, hasPopup = false }) {
 | |
|     const state = this.getState(policy, tab);
 | |
| 
 | |
|     const onHoverForAction = hasPopup
 | |
|       ? "origin-controls-state-runnable-hover-open"
 | |
|       : "origin-controls-state-runnable-hover-run";
 | |
| 
 | |
|     if (state.noAccess) {
 | |
|       return {
 | |
|         default: state.quarantined
 | |
|           ? "origin-controls-state-quarantined"
 | |
|           : "origin-controls-state-no-access",
 | |
|         onHover: isAction ? onHoverForAction : null,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     if (state.allDomains || (state.alwaysOn && state.hasAccess)) {
 | |
|       return {
 | |
|         default: "origin-controls-state-always-on",
 | |
|         onHover: isAction ? onHoverForAction : null,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     if (state.whenClicked) {
 | |
|       return {
 | |
|         default: state.temporaryAccess
 | |
|           ? "origin-controls-state-temporary-access"
 | |
|           : "origin-controls-state-when-clicked",
 | |
|         onHover: "origin-controls-state-hover-run-visit-only",
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   },
 | |
| };
 | |
| 
 | |
| export var QuarantinedDomains = {
 | |
|   getUserAllowedAddonIdPrefName(addonId) {
 | |
|     return `${this.PREF_ADDONS_BRANCH_NAME}${addonId}`;
 | |
|   },
 | |
|   isUserAllowedAddonId(addonId) {
 | |
|     return Services.prefs.getBoolPref(
 | |
|       this.getUserAllowedAddonIdPrefName(addonId),
 | |
|       false
 | |
|     );
 | |
|   },
 | |
|   setUserAllowedAddonIdPref(addonId, userAllowed) {
 | |
|     Services.prefs.setBoolPref(
 | |
|       this.getUserAllowedAddonIdPrefName(addonId),
 | |
|       userAllowed
 | |
|     );
 | |
|   },
 | |
|   clearUserPref(addonId) {
 | |
|     Services.prefs.clearUserPref(this.getUserAllowedAddonIdPrefName(addonId));
 | |
|   },
 | |
| 
 | |
|   // Implementation internals.
 | |
| 
 | |
|   PREF_ADDONS_BRANCH_NAME: `extensions.quarantineIgnoredByUser.`,
 | |
|   PREF_DOMAINSLIST_NAME: `extensions.quarantinedDomains.list`,
 | |
|   _initialized: false,
 | |
|   _init() {
 | |
|     if (this._initialized) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const onUserAllowedPrefChanged = this._onUserAllowedPrefChanged.bind(this);
 | |
|     Services.prefs.addObserver(
 | |
|       this.PREF_ADDONS_BRANCH_NAME,
 | |
|       onUserAllowedPrefChanged
 | |
|     );
 | |
| 
 | |
|     XPCOMUtils.defineLazyPreferenceGetter(
 | |
|       this,
 | |
|       "currentDomainsList",
 | |
|       this.PREF_DOMAINSLIST_NAME,
 | |
|       "",
 | |
|       null,
 | |
|       value => this._transformDomainsListPrefValue(value || "")
 | |
|     );
 | |
| 
 | |
|     this._initialized = true;
 | |
|   },
 | |
|   async _onUserAllowedPrefChanged(_subject, _topic, prefName) {
 | |
|     let addonId = prefName.slice(this.PREF_ADDONS_BRANCH_NAME.length);
 | |
|     // Sanity check.
 | |
|     if (!addonId || prefName !== this.getUserAllowedAddonIdPrefName(addonId)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Notify listeners, e.g. to update details in TelemetryEnvironment.
 | |
|     const addon = await lazy.AddonManager.getAddonByID(addonId);
 | |
|     // Do not call onPropertyChanged listeners if the addon cannot be found
 | |
|     // anymore (e.g. it has been uninstalled).
 | |
|     if (addon) {
 | |
|       lazy.AddonManagerPrivate.callAddonListeners("onPropertyChanged", addon, [
 | |
|         "quarantineIgnoredByUser",
 | |
|       ]);
 | |
|     }
 | |
|   },
 | |
|   _transformDomainsListPrefValue(value) {
 | |
|     try {
 | |
|       return {
 | |
|         set: new Set(
 | |
|           value
 | |
|             .split(",")
 | |
|             .map(v => v.trim())
 | |
|             .filter(v => v.length)
 | |
|         ),
 | |
|       };
 | |
|     } catch (err) {
 | |
|       return { hash: "unexpected-error", set: new Set() };
 | |
|     }
 | |
|   },
 | |
| };
 | |
| QuarantinedDomains._init();
 | |
| 
 | |
| // Constants exported for testing purpose.
 | |
| export {
 | |
|   OLD_JSON_FILENAME,
 | |
|   OLD_RKV_DIRNAME,
 | |
|   RKV_DIRNAME,
 | |
|   VERSION_KEY,
 | |
|   VERSION_VALUE,
 | |
| };
 | 
