668 lines
20 KiB
JavaScript
668 lines
20 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/. */
|
|
|
|
import { computeSha256HashAsString } from "resource://gre/modules/addons/crypto-utils.sys.mjs";
|
|
import {
|
|
GATED_PERMISSIONS,
|
|
SITEPERMS_ADDON_PROVIDER_PREF,
|
|
SITEPERMS_ADDON_TYPE,
|
|
isGatedPermissionType,
|
|
isKnownPublicSuffix,
|
|
isPrincipalInSitePermissionsBlocklist,
|
|
} from "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
|
|
});
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"addonsBundle",
|
|
() => new Localization(["toolkit/about/aboutAddons.ftl"], true)
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"SITEPERMS_ADDON_PROVIDER_ENABLED",
|
|
SITEPERMS_ADDON_PROVIDER_PREF,
|
|
false
|
|
);
|
|
|
|
const FIRST_CONTENT_PROCESS_TOPIC = "ipc:first-content-process-created";
|
|
const SITEPERMS_ADDON_ID_SUFFIX = "@siteperms.mozilla.org";
|
|
|
|
// Generate a per-session random salt, which is then used to generate
|
|
// per-siteOrigin hashed strings used as the addon id in SitePermsAddonWrapper constructor
|
|
// (expected to be matching new addon id generated for the same siteOrigin during
|
|
// the same browsing session and different ones in new browsing sessions).
|
|
//
|
|
// NOTE: `generateSalt` is exported for testing purpose, should not be
|
|
// used outside of tests.
|
|
let SALT;
|
|
export function generateSalt() {
|
|
// Throw if we're not in test and SALT is already defined
|
|
if (
|
|
typeof SALT !== "undefined" &&
|
|
!Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")
|
|
) {
|
|
throw new Error("This should only be called from XPCShell tests");
|
|
}
|
|
SALT = crypto.getRandomValues(new Uint8Array(12)).join("");
|
|
}
|
|
|
|
function getSalt() {
|
|
if (!SALT) {
|
|
generateSalt();
|
|
}
|
|
return SALT;
|
|
}
|
|
|
|
class SitePermsAddonWrapper {
|
|
// An array of nsIPermission granted for the siteOrigin.
|
|
// We can't use a Set as handlePermissionChange might be called with different
|
|
// nsIPermission instance for the same permission (in the generic sense)
|
|
#permissions = [];
|
|
|
|
// This will be set to true in the `uninstall` method to recognize when a perm-changed notification
|
|
// is actually triggered by the SitePermsAddonWrapper uninstall method itself.
|
|
isUninstalling = false;
|
|
|
|
/**
|
|
* @param {string} siteOriginNoSuffix: The origin this addon is installed for
|
|
* WITHOUT the suffix generated from the
|
|
* origin attributes (see:
|
|
* nsIPrincipal.siteOriginNoSuffix).
|
|
* @param {Array<nsIPermission>} permissions: An array of the initial
|
|
* permissions the user granted
|
|
* for the addon origin.
|
|
*/
|
|
constructor(siteOriginNoSuffix, permissions = []) {
|
|
this.siteOrigin = siteOriginNoSuffix;
|
|
this.principal =
|
|
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
this.siteOrigin
|
|
);
|
|
// Use a template string for the concat in case `siteOrigin` isn't a string.
|
|
const saltedValue = `${this.siteOrigin}${getSalt()}`;
|
|
this.id = `${computeSha256HashAsString(
|
|
saltedValue
|
|
)}${SITEPERMS_ADDON_ID_SUFFIX}`;
|
|
|
|
for (const perm of permissions) {
|
|
this.#permissions.push(perm);
|
|
}
|
|
}
|
|
|
|
get isUninstalled() {
|
|
return this.#permissions.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the list of gated permissions types granted for the instance's origin
|
|
*
|
|
* @return {Array<String>}
|
|
*/
|
|
get sitePermissions() {
|
|
return Array.from(new Set(this.#permissions.map(perm => perm.type)));
|
|
}
|
|
|
|
/**
|
|
* Update #permissions, and calls `uninstall` if there are no remaining gated permissions
|
|
* granted. This is called by SitePermsAddonProvider when it gets a "perm-changed" notification for a gated
|
|
* permission.
|
|
*
|
|
* @param {nsIPermission} permission: The permission being added/removed
|
|
* @param {String} action: The action perm-changed notifies us about
|
|
*/
|
|
handlePermissionChange(permission, action) {
|
|
if (action == "added") {
|
|
this.#permissions.push(permission);
|
|
} else if (action == "deleted") {
|
|
// We want to remove the registered permission for the right principal (looking into originSuffix so we
|
|
// can unregister revoked permission on a specific context, private window, ...).
|
|
this.#permissions = this.#permissions.filter(
|
|
perm =>
|
|
!(
|
|
perm.type == permission.type &&
|
|
perm.principal.originSuffix === permission.principal.originSuffix
|
|
)
|
|
);
|
|
|
|
if (this.#permissions.length === 0) {
|
|
this.uninstall();
|
|
}
|
|
}
|
|
}
|
|
|
|
get type() {
|
|
return SITEPERMS_ADDON_TYPE;
|
|
}
|
|
|
|
get name() {
|
|
return lazy.addonsBundle.formatValueSync("addon-sitepermission-host", {
|
|
host: this.principal.host,
|
|
});
|
|
}
|
|
|
|
get creator() {
|
|
return undefined;
|
|
}
|
|
|
|
get homepageURL() {
|
|
return undefined;
|
|
}
|
|
|
|
get description() {
|
|
return undefined;
|
|
}
|
|
|
|
get fullDescription() {
|
|
return undefined;
|
|
}
|
|
|
|
get version() {
|
|
// We consider the previous implementation attempt (signed addons) to be the initial version,
|
|
// hence the 2.0 for this approach.
|
|
return "2.0";
|
|
}
|
|
get updateDate() {
|
|
return undefined;
|
|
}
|
|
|
|
get isActive() {
|
|
return true;
|
|
}
|
|
|
|
get appDisabled() {
|
|
return false;
|
|
}
|
|
|
|
get userDisabled() {
|
|
return false;
|
|
}
|
|
set userDisabled(aVal) {}
|
|
|
|
get size() {
|
|
return 0;
|
|
}
|
|
|
|
async updateBlocklistState() {}
|
|
|
|
get blocklistState() {
|
|
return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
|
|
}
|
|
|
|
get scope() {
|
|
return lazy.AddonManager.SCOPE_APPLICATION;
|
|
}
|
|
|
|
get pendingOperations() {
|
|
return lazy.AddonManager.PENDING_NONE;
|
|
}
|
|
|
|
get operationsRequiringRestart() {
|
|
return lazy.AddonManager.OP_NEEDS_RESTART_NONE;
|
|
}
|
|
|
|
get permissions() {
|
|
// The addon only supports PERM_CAN_UNINSTALL and no other AOM permission.
|
|
return lazy.AddonManager.PERM_CAN_UNINSTALL;
|
|
}
|
|
|
|
get signedState() {
|
|
// Will make the permission prompt use the webextSitePerms.headerUnsignedWithPerms string
|
|
return lazy.AddonManager.SIGNEDSTATE_MISSING;
|
|
}
|
|
|
|
async enable() {}
|
|
|
|
async disable() {}
|
|
|
|
/**
|
|
* Uninstall the addon, calling AddonManager hooks and removing all granted permissions.
|
|
*
|
|
* @throws Services.perms.removeFromPrincipal could throw, see PermissionManager::AddInternal.
|
|
*/
|
|
async uninstall() {
|
|
if (this.isUninstalling) {
|
|
return;
|
|
}
|
|
try {
|
|
this.isUninstalling = true;
|
|
lazy.AddonManagerPrivate.callAddonListeners(
|
|
"onUninstalling",
|
|
this,
|
|
false
|
|
);
|
|
const permissions = [...this.#permissions];
|
|
for (const permission of permissions) {
|
|
try {
|
|
Services.perms.removeFromPrincipal(
|
|
permission.principal,
|
|
permission.type
|
|
);
|
|
// Only remove the permission from the array if it was successfully removed from the principal
|
|
this.#permissions.splice(this.#permissions.indexOf(permission), 1);
|
|
} catch (err) {
|
|
Cu.reportError(err);
|
|
}
|
|
}
|
|
lazy.AddonManagerPrivate.callAddonListeners("onUninstalled", this);
|
|
} finally {
|
|
this.isUninstalling = false;
|
|
}
|
|
}
|
|
|
|
get isCompatible() {
|
|
return true;
|
|
}
|
|
|
|
get isPlatformCompatible() {
|
|
return true;
|
|
}
|
|
|
|
get providesUpdatesSecurely() {
|
|
return true;
|
|
}
|
|
|
|
get foreignInstall() {
|
|
return false;
|
|
}
|
|
|
|
get installTelemetryInfo() {
|
|
return { source: "siteperm-addon-provider", method: "synthetic-install" };
|
|
}
|
|
|
|
isCompatibleWith() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class SitePermsAddonInstalling extends SitePermsAddonWrapper {
|
|
/**
|
|
* @param {string} siteOriginNoSuffix: The origin this addon is installed
|
|
* for, WITHOUT the suffix generated from
|
|
* the origin attributes (see:
|
|
* nsIPrincipal.siteOriginNoSuffix).
|
|
* @param {SitePermsAddonInstall} install: The SitePermsAddonInstall instance
|
|
* calling this constructor.
|
|
*/
|
|
constructor(siteOriginNoSuffix, install) {
|
|
// SitePermsAddonWrapper expect an array of nsIPermission as its second parameter.
|
|
// Since we don't have a proper permission here, we pass an object with the properties
|
|
// being used in the class.
|
|
const permission = {
|
|
principal: install.principal,
|
|
type: install.newSitePerm,
|
|
};
|
|
|
|
super(siteOriginNoSuffix, [permission]);
|
|
}
|
|
|
|
get existingAddon() {
|
|
return SitePermsAddonProvider.wrappersMapByOrigin.get(this.siteOrigin);
|
|
}
|
|
|
|
uninstall() {
|
|
// While about:addons tab is already open, new addon cards for newly installed
|
|
// addons are created from the `onInstalled` AOM events, for the `SitePermsAddonWrapper`
|
|
// the `onInstalling` and `onInstalled` events are emitted by `SitePermsAddonInstall`
|
|
// and the addon instance is going to be a `SitePermsAddonInstalling` instance if
|
|
// there wasn't an AddonCard for the same addon id yet.
|
|
//
|
|
// To make sure that all permissions will be uninstalled if a user uninstall the
|
|
// addon from an AddonCard created from a `SitePermsAddonInstalling` instance,
|
|
// we forward calls to the uninstall method of the existing `SitePermsAddonWrapper`
|
|
// instance being tracked by the `SitePermsAddonProvider`.
|
|
// If there isn't any then removing only the single permission added along with the
|
|
// `SitePremsAddonInstalling` is going to be enough.
|
|
if (this.existingAddon) {
|
|
return this.existingAddon.uninstall();
|
|
}
|
|
|
|
return super.uninstall();
|
|
}
|
|
|
|
validInstallOrigins() {
|
|
// Always return true from here,
|
|
// actual checks are done from AddonManagerInternal.getSitePermsAddonInstallForWebpage
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Numeric id included in the install telemetry events to correlate multiple events related
|
|
// to the same install or update flow.
|
|
let nextInstallId = 0;
|
|
|
|
class SitePermsAddonInstall {
|
|
#listeners = new Set();
|
|
#installEvents = {
|
|
INSTALL_CANCELLED: "onInstallCancelled",
|
|
INSTALL_ENDED: "onInstallEnded",
|
|
INSTALL_FAILED: "onInstallFailed",
|
|
};
|
|
|
|
/**
|
|
* @param {nsIPrincipal} installingPrincipal
|
|
* @param {String} sitePerm
|
|
*/
|
|
constructor(installingPrincipal, sitePerm) {
|
|
this.principal = installingPrincipal;
|
|
this.newSitePerm = sitePerm;
|
|
this.state = lazy.AddonManager.STATE_DOWNLOADED;
|
|
this.addon = new SitePermsAddonInstalling(
|
|
this.principal.siteOriginNoSuffix,
|
|
this
|
|
);
|
|
this.installId = ++nextInstallId;
|
|
}
|
|
|
|
get installTelemetryInfo() {
|
|
return this.addon.installTelemetryInfo;
|
|
}
|
|
|
|
async checkPrompt() {
|
|
// `promptHandler` can be set from `AddonManagerInternal.setupPromptHandler`
|
|
if (this.promptHandler) {
|
|
let info = {
|
|
// TODO: Investigate if we need to handle addon "update", i.e. granting new
|
|
// gated permission on an origin other permissions were already granted for (Bug 1790778).
|
|
existingAddon: null,
|
|
addon: this.addon,
|
|
icon: "chrome://mozapps/skin/extensions/category-sitepermission.svg",
|
|
// Used in AMTelemetry to detect the install flow related to this prompt.
|
|
install: this,
|
|
};
|
|
|
|
try {
|
|
await this.promptHandler(info);
|
|
} catch (err) {
|
|
if (this.error < 0) {
|
|
this.state = lazy.AddonManager.STATE_INSTALL_FAILED;
|
|
// In some cases onOperationCancelled is called during failures
|
|
// to install/uninstall/enable/disable addons. We may need to
|
|
// do that here in the future.
|
|
this.#callInstallListeners(this.#installEvents.INSTALL_FAILED);
|
|
} else if (this.state !== lazy.AddonManager.STATE_CANCELLED) {
|
|
this.cancel();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.state = lazy.AddonManager.STATE_PROMPTS_DONE;
|
|
this.install();
|
|
}
|
|
|
|
install() {
|
|
if (this.state === lazy.AddonManager.STATE_PROMPTS_DONE) {
|
|
lazy.AddonManagerPrivate.callAddonListeners("onInstalling", this.addon);
|
|
Services.perms.addFromPrincipal(
|
|
this.principal,
|
|
this.newSitePerm,
|
|
Services.perms.ALLOW_ACTION
|
|
);
|
|
this.state = lazy.AddonManager.STATE_INSTALLED;
|
|
this.#callInstallListeners(this.#installEvents.INSTALL_ENDED);
|
|
lazy.AddonManagerPrivate.callAddonListeners("onInstalled", this.addon);
|
|
this.addon.install = null;
|
|
return;
|
|
}
|
|
|
|
if (this.state !== lazy.AddonManager.STATE_DOWNLOADED) {
|
|
this.state = lazy.AddonManager.STATE_INSTALL_FAILED;
|
|
this.#callInstallListeners(this.#installEvents.INSTALL_FAILED);
|
|
return;
|
|
}
|
|
|
|
this.checkPrompt();
|
|
}
|
|
|
|
cancel() {
|
|
// This method can be called if the install is already cancelled.
|
|
// We don't want to go further in such case as it would lead to duplicated Telemetry events.
|
|
if (this.state == lazy.AddonManager.STATE_CANCELLED) {
|
|
console.error("SitePermsAddonInstall#cancel called twice on ", this);
|
|
return;
|
|
}
|
|
|
|
this.state = lazy.AddonManager.STATE_CANCELLED;
|
|
this.#callInstallListeners(this.#installEvents.INSTALL_CANCELLED);
|
|
}
|
|
|
|
/**
|
|
* Add a listener for the install events
|
|
*
|
|
* @param {Object} listener
|
|
* @param {Function} [listener.onDownloadEnded]
|
|
* @param {Function} [listener.onInstallCancelled]
|
|
* @param {Function} [listener.onInstallEnded]
|
|
* @param {Function} [listener.onInstallFailed]
|
|
*/
|
|
addListener(listener) {
|
|
this.#listeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove a listener
|
|
*
|
|
* @param {Object} listener: The same object reference that was used for `addListener`
|
|
*/
|
|
removeListener(listener) {
|
|
this.#listeners.delete(listener);
|
|
}
|
|
|
|
/**
|
|
* Call the listeners callbacks for a given event.
|
|
*
|
|
* @param {String} eventName: The event to fire. Should be one of `this.#installEvents`
|
|
*/
|
|
#callInstallListeners(eventName) {
|
|
if (!Object.values(this.#installEvents).includes(eventName)) {
|
|
console.warn(`Unknown "${eventName}" "event`);
|
|
return;
|
|
}
|
|
|
|
lazy.AddonManagerPrivate.callInstallListeners(
|
|
eventName,
|
|
Array.from(this.#listeners),
|
|
this
|
|
);
|
|
}
|
|
}
|
|
|
|
const SitePermsAddonProvider = {
|
|
get name() {
|
|
return "SitePermsAddonProvider";
|
|
},
|
|
|
|
wrappersMapByOrigin: new Map(),
|
|
|
|
/**
|
|
* Update wrappersMapByOrigin on perm-changed
|
|
*
|
|
* @param {nsIPermission} permission: The permission being added/removed
|
|
* @param {String} action: The action perm-changed notifies us about
|
|
*/
|
|
handlePermissionChange(permission, action = "added") {
|
|
// Bail out if it it's not a gated perm
|
|
if (!isGatedPermissionType(permission.type)) {
|
|
return;
|
|
}
|
|
|
|
// Gated APIs should probably not be available on non-secure origins,
|
|
// but let's double check here.
|
|
if (permission.principal.scheme !== "https") {
|
|
return;
|
|
}
|
|
|
|
if (isPrincipalInSitePermissionsBlocklist(permission.principal)) {
|
|
return;
|
|
}
|
|
|
|
const { siteOriginNoSuffix } = permission.principal;
|
|
|
|
// Install origin cannot be on a known etld (e.g. github.io).
|
|
// We shouldn't get a permission change for those here, but let's
|
|
// be extra safe
|
|
if (isKnownPublicSuffix(siteOriginNoSuffix)) {
|
|
return;
|
|
}
|
|
|
|
// Pipe the change to the existing addon if there is one.
|
|
if (this.wrappersMapByOrigin.has(siteOriginNoSuffix)) {
|
|
this.wrappersMapByOrigin
|
|
.get(siteOriginNoSuffix)
|
|
.handlePermissionChange(permission, action);
|
|
}
|
|
|
|
if (action == "added") {
|
|
// We only have one SitePermsAddon per origin, handling multiple permissions.
|
|
if (this.wrappersMapByOrigin.has(siteOriginNoSuffix)) {
|
|
return;
|
|
}
|
|
|
|
const addonWrapper = new SitePermsAddonWrapper(siteOriginNoSuffix, [
|
|
permission,
|
|
]);
|
|
this.wrappersMapByOrigin.set(siteOriginNoSuffix, addonWrapper);
|
|
return;
|
|
}
|
|
|
|
if (action == "deleted") {
|
|
if (!this.wrappersMapByOrigin.has(siteOriginNoSuffix)) {
|
|
return;
|
|
}
|
|
// Only remove the addon if it doesn't have any permissions left.
|
|
if (!this.wrappersMapByOrigin.get(siteOriginNoSuffix).isUninstalled) {
|
|
return;
|
|
}
|
|
this.wrappersMapByOrigin.delete(siteOriginNoSuffix);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a Promise that resolves when handled the list of gated permissions
|
|
* and setup ther observer for the "perm-changed" event.
|
|
*
|
|
* @returns Promise
|
|
*/
|
|
lazyInit() {
|
|
if (!this._initPromise) {
|
|
this._initPromise = new Promise(resolve => {
|
|
// Build the initial list of addons per origin
|
|
const perms = Services.perms.getAllByTypes(GATED_PERMISSIONS);
|
|
for (const perm of perms) {
|
|
this.handlePermissionChange(perm);
|
|
}
|
|
Services.obs.addObserver(this, "perm-changed");
|
|
resolve();
|
|
});
|
|
}
|
|
return this._initPromise;
|
|
},
|
|
|
|
shutdown() {
|
|
if (this._initPromise) {
|
|
Services.obs.removeObserver(this, "perm-changed");
|
|
}
|
|
this.wrappersMapByOrigin.clear();
|
|
this._initPromise = null;
|
|
},
|
|
|
|
/**
|
|
* Get a SitePermsAddonWrapper from an extension id
|
|
*
|
|
* @param {String|null|undefined} id: The extension id,
|
|
* @returns {SitePermsAddonWrapper|undefined}
|
|
*/
|
|
async getAddonByID(id) {
|
|
await this.lazyInit();
|
|
if (!id?.endsWith?.(SITEPERMS_ADDON_ID_SUFFIX)) {
|
|
return undefined;
|
|
}
|
|
|
|
for (const addon of this.wrappersMapByOrigin.values()) {
|
|
if (addon.id === id) {
|
|
return addon;
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Get a list of SitePermsAddonWrapper for a given list of extension types.
|
|
*
|
|
* @param {Array<String>|null|undefined} types: If null or undefined is passed,
|
|
* the callsites expect to get all the addons from the provider, without
|
|
* any filtering.
|
|
* @returns {Array<SitePermsAddonWrapper>}
|
|
*/
|
|
async getAddonsByTypes(types) {
|
|
if (
|
|
!this.isEnabled ||
|
|
// `types` can be null/undefined, and in such case we _do_ want to return the addons.
|
|
(Array.isArray(types) && !types.includes(SITEPERMS_ADDON_TYPE))
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
await this.lazyInit();
|
|
return Array.from(this.wrappersMapByOrigin.values());
|
|
},
|
|
|
|
/**
|
|
* Create and return a SitePermsAddonInstall instance for a permission on a given principal
|
|
*
|
|
* @param {nsIPrincipal} installingPrincipal
|
|
* @param {String} sitePerm
|
|
* @returns {SitePermsAddonInstall}
|
|
*/
|
|
getSitePermsAddonInstallForWebpage(installingPrincipal, sitePerm) {
|
|
return new SitePermsAddonInstall(installingPrincipal, sitePerm);
|
|
},
|
|
|
|
get isEnabled() {
|
|
return lazy.SITEPERMS_ADDON_PROVIDER_ENABLED;
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (topic == FIRST_CONTENT_PROCESS_TOPIC) {
|
|
Services.obs.removeObserver(this, FIRST_CONTENT_PROCESS_TOPIC);
|
|
|
|
lazy.AddonManagerPrivate.registerProvider(SitePermsAddonProvider, [
|
|
SITEPERMS_ADDON_TYPE,
|
|
]);
|
|
Services.obs.notifyObservers(null, "sitepermsaddon-provider-registered");
|
|
} else if (topic === "perm-changed") {
|
|
if (data === "cleared") {
|
|
// In such case, `subject` is null, but we can simply uninstall all existing addons.
|
|
for (const addon of this.wrappersMapByOrigin.values()) {
|
|
addon.uninstall();
|
|
}
|
|
this.wrappersMapByOrigin.clear();
|
|
return;
|
|
}
|
|
|
|
const perm = subject.QueryInterface(Ci.nsIPermission);
|
|
this.handlePermissionChange(perm, data);
|
|
}
|
|
},
|
|
|
|
addFirstContentProcessObserver() {
|
|
Services.obs.addObserver(this, FIRST_CONTENT_PROCESS_TOPIC);
|
|
},
|
|
};
|
|
|
|
// We want to register the SitePermsAddonProvider once the first content process gets created
|
|
// (and only if the feature is also enabled through the "dom.sitepermsaddon-provider.enabled"
|
|
// about:config pref).
|
|
SitePermsAddonProvider.addFirstContentProcessObserver();
|