gecko-dev/browser/modules/SitePermissions.sys.mjs

1336 lines
41 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
var gStringBundle = Services.strings.createBundle(
"chrome://browser/locale/sitePermissions.properties"
);
/**
* A helper module to manage temporary permissions.
*
* Permissions are keyed by browser, so methods take a Browser
* element to identify the corresponding permission set.
*
* This uses a WeakMap to key browsers, so that entries are
* automatically cleared once the browser stops existing
* (once there are no other references to the browser object);
*/
const TemporaryPermissions = {
// This is a three level deep map with the following structure:
//
// Browser => {
// <baseDomain|origin>: {
// <permissionID>: {state: Number, expireTimeout: Number}
// }
// }
//
// Only the top level browser elements are stored via WeakMap. The WeakMap
// value is an object with URI baseDomains or origins as keys. The keys of
// that object are ids that identify permissions that were set for the
// specific URI. The final value is an object containing the permission state
// and the id of the timeout which will cause permission expiry.
// BLOCK permissions are keyed under baseDomain to prevent bypassing the block
// (see Bug 1492668). Any other permissions are keyed under origin.
_stateByBrowser: new WeakMap(),
// Extract baseDomain from uri. Fallback to hostname on conversion error.
_uriToBaseDomain(uri) {
try {
return Services.eTLD.getBaseDomain(uri);
} catch (error) {
if (
error.result !== Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
error.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
) {
throw error;
}
return uri.host;
}
},
/**
* Generate keys to store temporary permissions under. The strict key is
* origin, non-strict is baseDomain.
* @param {nsIPrincipal} principal - principal to derive keys from.
* @returns {Object} keys - Object containing the generated permission keys.
* @returns {string} keys.strict - Key to be used for strict matching.
* @returns {string} keys.nonStrict - Key to be used for non-strict matching.
* @throws {Error} - Throws if principal is undefined or no valid permission key can
* be generated.
*/
_getKeysFromPrincipal(principal) {
return { strict: principal.origin, nonStrict: principal.baseDomain };
},
/**
* Sets a new permission for the specified browser.
* @returns {boolean} whether the permission changed, effectively.
*/
set(
browser,
id,
state,
expireTimeMS,
principal = browser.contentPrincipal,
expireCallback
) {
if (
!browser ||
!principal ||
!SitePermissions.isSupportedPrincipal(principal)
) {
return false;
}
let entry = this._stateByBrowser.get(browser);
if (!entry) {
entry = { browser: Cu.getWeakReference(browser), uriToPerm: {} };
this._stateByBrowser.set(browser, entry);
}
let { uriToPerm } = entry;
// We store blocked permissions by baseDomain. Other states by origin.
let { strict, nonStrict } = this._getKeysFromPrincipal(principal);
let setKey;
let deleteKey;
// Differentiate between block and non-block permissions. If we store a
// block permission we need to delete old entries which may be set under
// origin before setting the new permission for baseDomain. For non-block
// permissions this is swapped.
if (state == SitePermissions.BLOCK) {
setKey = nonStrict;
deleteKey = strict;
} else {
setKey = strict;
deleteKey = nonStrict;
}
if (!uriToPerm[setKey]) {
uriToPerm[setKey] = {};
}
let expireTimeout = uriToPerm[setKey][id]?.expireTimeout;
let previousState = uriToPerm[setKey][id]?.state;
// If overwriting a permission state. We need to cancel the old timeout.
if (expireTimeout) {
lazy.clearTimeout(expireTimeout);
}
// Construct the new timeout to remove the permission once it has expired.
expireTimeout = lazy.setTimeout(() => {
let entryBrowser = entry.browser.get();
// Exit early if the browser is no longer alive when we get the timeout
// callback.
if (!entryBrowser || !uriToPerm[setKey]) {
return;
}
delete uriToPerm[setKey][id];
// Notify SitePermissions that a temporary permission has expired.
// Get the browser the permission is currently set for. If this.copy was
// used this browser is different from the original one passed above.
expireCallback(entryBrowser);
}, expireTimeMS);
uriToPerm[setKey][id] = {
expireTimeout,
state,
};
// If we set a permission state for a origin we need to reset the old state
// which may be set for baseDomain and vice versa. An individual permission
// must only ever be keyed by either origin or baseDomain.
let permissions = uriToPerm[deleteKey];
if (permissions) {
expireTimeout = permissions[id]?.expireTimeout;
if (expireTimeout) {
lazy.clearTimeout(expireTimeout);
}
delete permissions[id];
}
return state != previousState;
},
/**
* Removes a permission with the specified id for the specified browser.
* @returns {boolean} whether the permission was removed.
*/
remove(browser, id) {
if (
!browser ||
!SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
!this._stateByBrowser.has(browser)
) {
return false;
}
// Permission can be stored by any of the two keys (strict and non-strict).
// getKeysFromURI can throw. We let the caller handle the exception.
let { strict, nonStrict } = this._getKeysFromPrincipal(
browser.contentPrincipal
);
let { uriToPerm } = this._stateByBrowser.get(browser);
for (let key of [nonStrict, strict]) {
if (uriToPerm[key]?.[id] != null) {
let { expireTimeout } = uriToPerm[key][id];
if (expireTimeout) {
lazy.clearTimeout(expireTimeout);
}
delete uriToPerm[key][id];
// Individual permissions can only ever be keyed either strict or
// non-strict. If we find the permission via the first key run we can
// return early.
return true;
}
}
return false;
},
// Gets a permission with the specified id for the specified browser.
get(browser, id) {
if (
!browser ||
!browser.contentPrincipal ||
!SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
!this._stateByBrowser.has(browser)
) {
return null;
}
let { uriToPerm } = this._stateByBrowser.get(browser);
let { strict, nonStrict } = this._getKeysFromPrincipal(
browser.contentPrincipal
);
for (let key of [nonStrict, strict]) {
if (uriToPerm[key]) {
let permission = uriToPerm[key][id];
if (permission) {
return {
id,
state: permission.state,
scope: SitePermissions.SCOPE_TEMPORARY,
};
}
}
}
return null;
},
// Gets all permissions for the specified browser.
// Note that only permissions that apply to the current URI
// of the passed browser element will be returned.
getAll(browser) {
let permissions = [];
if (
!SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
!this._stateByBrowser.has(browser)
) {
return permissions;
}
let { uriToPerm } = this._stateByBrowser.get(browser);
let { strict, nonStrict } = this._getKeysFromPrincipal(
browser.contentPrincipal
);
for (let key of [nonStrict, strict]) {
if (uriToPerm[key]) {
let perms = uriToPerm[key];
for (let id of Object.keys(perms)) {
let permission = perms[id];
if (permission) {
permissions.push({
id,
state: permission.state,
scope: SitePermissions.SCOPE_TEMPORARY,
});
}
}
}
}
return permissions;
},
// Clears all permissions for the specified browser.
// Unlike other methods, this does NOT clear only for
// the currentURI but the whole browser state.
/**
* Clear temporary permissions for the specified browser. Unlike other
* methods, this does NOT clear only for the currentURI but the whole browser
* state.
* @param {Browser} browser - Browser to clear permissions for.
* @param {Number} [filterState] - Only clear permissions with the given state
* value. Defaults to all permissions.
*/
clear(browser, filterState = null) {
let entry = this._stateByBrowser.get(browser);
if (!entry?.uriToPerm) {
return;
}
let { uriToPerm } = entry;
Object.entries(uriToPerm).forEach(([uriKey, permissions]) => {
Object.entries(permissions).forEach(
([permId, { state, expireTimeout }]) => {
// We need to explicitly check for null or undefined here, because the
// permission state may be 0.
if (filterState != null) {
if (state != filterState) {
// Skip permission entry if it doesn't match the filter.
return;
}
delete permissions[permId];
}
// For the clear-all case we remove the entire browser entry, so we
// only need to clear the timeouts.
if (!expireTimeout) {
return;
}
lazy.clearTimeout(expireTimeout);
}
);
// If there are no more permissions, remove the entry from the URI map.
if (filterState != null && !Object.keys(permissions).length) {
delete uriToPerm[uriKey];
}
});
// We're either clearing all permissions or only the permissions with state
// == filterState. If we have a filter, we can only clean up the browser if
// there are no permission entries left in the map.
if (filterState == null || !Object.keys(uriToPerm).length) {
this._stateByBrowser.delete(browser);
}
},
// Copies the temporary permission state of one browser
// into a new entry for the other browser.
copy(browser, newBrowser) {
let entry = this._stateByBrowser.get(browser);
if (entry) {
entry.browser = Cu.getWeakReference(newBrowser);
this._stateByBrowser.set(newBrowser, entry);
}
},
};
// This hold a flag per browser to indicate whether we should show the
// user a notification as a permission has been requested that has been
// blocked globally. We only want to notify the user in the case that
// they actually requested the permission within the current page load
// so will clear the flag on navigation.
const GloballyBlockedPermissions = {
_stateByBrowser: new WeakMap(),
/**
* @returns {boolean} whether the permission was removed.
*/
set(browser, id) {
if (!this._stateByBrowser.has(browser)) {
this._stateByBrowser.set(browser, {});
}
let entry = this._stateByBrowser.get(browser);
let origin = browser.contentPrincipal.origin;
if (!entry[origin]) {
entry[origin] = {};
}
if (entry[origin][id]) {
return false;
}
entry[origin][id] = true;
// Clear the flag and remove the listener once the user has navigated.
// WebProgress will report various things including hashchanges to us, the
// navigation we care about is either leaving the current page or reloading.
let { prePath } = browser.currentURI;
browser.addProgressListener(
{
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
let hasLeftPage =
aLocation.prePath != prePath ||
!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
let isReload = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
);
if (aWebProgress.isTopLevel && (hasLeftPage || isReload)) {
GloballyBlockedPermissions.remove(browser, id, origin);
browser.removeProgressListener(this);
}
},
},
Ci.nsIWebProgress.NOTIFY_LOCATION
);
return true;
},
// Removes a permission with the specified id for the specified browser.
remove(browser, id, origin = null) {
let entry = this._stateByBrowser.get(browser);
if (!origin) {
origin = browser.contentPrincipal.origin;
}
if (entry && entry[origin]) {
delete entry[origin][id];
}
},
// Gets all permissions for the specified browser.
// Note that only permissions that apply to the current URI
// of the passed browser element will be returned.
getAll(browser) {
let permissions = [];
let entry = this._stateByBrowser.get(browser);
let origin = browser.contentPrincipal.origin;
if (entry && entry[origin]) {
let timeStamps = entry[origin];
for (let id of Object.keys(timeStamps)) {
permissions.push({
id,
state: gPermissions.get(id).getDefault(),
scope: SitePermissions.SCOPE_GLOBAL,
});
}
}
return permissions;
},
// Copies the globally blocked permission state of one browser
// into a new entry for the other browser.
copy(browser, newBrowser) {
let entry = this._stateByBrowser.get(browser);
if (entry) {
this._stateByBrowser.set(newBrowser, entry);
}
},
};
/**
* A module to manage permanent and temporary permissions
* by URI and browser.
*
* Some methods have the side effect of dispatching a "PermissionStateChange"
* event on changes to temporary permissions, as mentioned in the respective docs.
*/
export var SitePermissions = {
// Permission states.
UNKNOWN: Services.perms.UNKNOWN_ACTION,
ALLOW: Services.perms.ALLOW_ACTION,
BLOCK: Services.perms.DENY_ACTION,
PROMPT: Services.perms.PROMPT_ACTION,
ALLOW_COOKIES_FOR_SESSION: Ci.nsICookiePermission.ACCESS_SESSION,
AUTOPLAY_BLOCKED_ALL: Ci.nsIAutoplay.BLOCKED_ALL,
// Permission scopes.
SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}",
SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}",
SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
// The delimiter used for double keyed permissions.
// For example: open-protocol-handler^irc
PERM_KEY_DELIMITER: "^",
_permissionsArray: null,
_defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
// For testing use only.
_temporaryPermissions: TemporaryPermissions,
/**
* Gets all custom permissions for a given principal.
* Install addon permission is excluded, check bug 1303108.
*
* @return {Array} a list of objects with the keys:
* - id: the permissionId of the permission
* - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
* - state: a constant representing the current permission state
* (e.g. SitePermissions.ALLOW)
*/
getAllByPrincipal(principal) {
if (!principal) {
throw new Error("principal argument cannot be null.");
}
if (!this.isSupportedPrincipal(principal)) {
return [];
}
// Get all permissions from the permission manager by principal, excluding
// the ones set to be disabled.
let permissions = Services.perms
.getAllForPrincipal(principal)
.filter(permission => {
let entry = gPermissions.get(permission.type);
if (!entry || entry.disabled) {
return false;
}
let type = entry.id;
/* Hide persistent storage permission when extension principal
* have WebExtensions-unlimitedStorage permission. */
if (
type == "persistent-storage" &&
SitePermissions.getForPrincipal(
principal,
"WebExtensions-unlimitedStorage"
).state == SitePermissions.ALLOW
) {
return false;
}
return true;
});
return permissions.map(permission => {
let scope = this.SCOPE_PERSISTENT;
if (permission.expireType == Services.perms.EXPIRE_SESSION) {
scope = this.SCOPE_SESSION;
} else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
scope = this.SCOPE_POLICY;
}
return {
id: permission.type,
scope,
state: permission.capability,
};
});
},
/**
* Returns all custom permissions for a given browser.
*
* To receive a more detailed, albeit less performant listing see
* SitePermissions.getAllPermissionDetailsForBrowser().
*
* @param {Browser} browser
* The browser to fetch permission for.
*
* @return {Array} a list of objects with the keys:
* - id: the permissionId of the permission
* - state: a constant representing the current permission state
* (e.g. SitePermissions.ALLOW)
* - scope: a constant representing how long the permission will
* be kept.
*/
getAllForBrowser(browser) {
let permissions = {};
for (let permission of TemporaryPermissions.getAll(browser)) {
permission.scope = this.SCOPE_TEMPORARY;
permissions[permission.id] = permission;
}
for (let permission of GloballyBlockedPermissions.getAll(browser)) {
permissions[permission.id] = permission;
}
for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) {
permissions[permission.id] = permission;
}
return Object.values(permissions);
},
/**
* Returns a list of objects with detailed information on all permissions
* that are currently set for the given browser.
*
* @param {Browser} browser
* The browser to fetch permission for.
*
* @return {Array<Object>} a list of objects with the keys:
* - id: the permissionID of the permission
* - state: a constant representing the current permission state
* (e.g. SitePermissions.ALLOW)
* - scope: a constant representing how long the permission will
* be kept.
* - label: the localized label, or null if none is available.
*/
getAllPermissionDetailsForBrowser(browser) {
return this.getAllForBrowser(browser).map(({ id, scope, state }) => ({
id,
scope,
state,
label: this.getPermissionLabel(id),
}));
},
/**
* Checks whether a UI for managing permissions should be exposed for a given
* principal.
*
* @param {nsIPrincipal} principal
* The principal to check.
*
* @return {boolean} if the principal is supported.
*/
isSupportedPrincipal(principal) {
if (!principal) {
return false;
}
if (!(principal instanceof Ci.nsIPrincipal)) {
throw new Error(
"Argument passed as principal is not an instance of Ci.nsIPrincipal"
);
}
return this.isSupportedScheme(principal.scheme);
},
/**
* Checks whether we support managing permissions for a specific scheme.
* @param {string} scheme - Scheme to test.
* @returns {boolean} Whether the scheme is supported.
*/
isSupportedScheme(scheme) {
return ["http", "https", "moz-extension", "file"].includes(scheme);
},
/**
* Gets an array of all permission IDs.
*
* @return {Array<String>} an array of all permission IDs.
*/
listPermissions() {
if (this._permissionsArray === null) {
this._permissionsArray = gPermissions.getEnabledPermissions();
}
return this._permissionsArray;
},
/**
* Test whether a permission is managed by SitePermissions.
* @param {string} type - Permission type.
* @returns {boolean}
*/
isSitePermission(type) {
return gPermissions.has(type);
},
/**
* Called when a preference changes its value.
*
* @param {string} data
* The last argument passed to the preference change observer
* @param {string} previous
* The previous value of the preference
* @param {string} latest
* The latest value of the preference
*/
invalidatePermissionList() {
// Ensure that listPermissions() will reconstruct its return value the next
// time it's called.
this._permissionsArray = null;
},
/**
* Returns an array of permission states to be exposed to the user for a
* permission with the given ID.
*
* @param {string} permissionID
* The ID to get permission states for.
*
* @return {Array<SitePermissions state>} an array of all permission states.
*/
getAvailableStates(permissionID) {
if (
gPermissions.has(permissionID) &&
gPermissions.get(permissionID).states
) {
return gPermissions.get(permissionID).states;
}
/* Since the permissions we are dealing with have adopted the convention
* of treating UNKNOWN == PROMPT, we only include one of either UNKNOWN
* or PROMPT in this list, to avoid duplicating states. */
if (this.getDefault(permissionID) == this.UNKNOWN) {
return [
SitePermissions.UNKNOWN,
SitePermissions.ALLOW,
SitePermissions.BLOCK,
];
}
return [
SitePermissions.PROMPT,
SitePermissions.ALLOW,
SitePermissions.BLOCK,
];
},
/**
* Returns the default state of a particular permission.
*
* @param {string} permissionID
* The ID to get the default for.
*
* @return {SitePermissions.state} the default state.
*/
getDefault(permissionID) {
// If the permission has custom logic for getting its default value,
// try that first.
if (
gPermissions.has(permissionID) &&
gPermissions.get(permissionID).getDefault
) {
return gPermissions.get(permissionID).getDefault();
}
// Otherwise try to get the default preference for that permission.
return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
},
/**
* Set the default state of a particular permission.
*
* @param {string} permissionID
* The ID to set the default for.
*
* @param {string} state
* The state to set.
*/
setDefault(permissionID, state) {
if (
gPermissions.has(permissionID) &&
gPermissions.get(permissionID).setDefault
) {
return gPermissions.get(permissionID).setDefault(state);
}
let key = "permissions.default." + permissionID;
return Services.prefs.setIntPref(key, state);
},
/**
* Returns the state and scope of a particular permission for a given principal.
*
* This method will NOT dispatch a "PermissionStateChange" event on the specified
* browser if a temporary permission was removed because it has expired.
*
* @param {nsIPrincipal} principal
* The principal to check.
* @param {String} permissionID
* The id of the permission.
* @param {Browser} [browser] The browser object to check for temporary
* permissions.
*
* @return {Object} an object with the keys:
* - state: The current state of the permission
* (e.g. SitePermissions.ALLOW)
* - scope: The scope of the permission
* (e.g. SitePermissions.SCOPE_PERSISTENT)
*/
getForPrincipal(principal, permissionID, browser) {
if (!principal && !browser) {
throw new Error(
"Atleast one of the arguments, either principal or browser should not be null."
);
}
let defaultState = this.getDefault(permissionID);
let result = { state: defaultState, scope: this.SCOPE_PERSISTENT };
if (this.isSupportedPrincipal(principal)) {
let permission = null;
if (
gPermissions.has(permissionID) &&
gPermissions.get(permissionID).exactHostMatch
) {
permission = Services.perms.getPermissionObject(
principal,
permissionID,
true
);
} else {
permission = Services.perms.getPermissionObject(
principal,
permissionID,
false
);
}
if (permission) {
result.state = permission.capability;
if (permission.expireType == Services.perms.EXPIRE_SESSION) {
result.scope = this.SCOPE_SESSION;
} else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
result.scope = this.SCOPE_POLICY;
}
}
}
if (
result.state == defaultState ||
result.state == SitePermissions.PROMPT
) {
// If there's no persistent permission saved, or if the persistent permission
// saved is merely PROMPT (aka "Always Ask" when persisted for camera and
// microphone), then check if we have something set temporarily.
//
// This way, a temporary ALLOW or BLOCK trumps a persisted PROMPT. While
// having overlap would be a bug (because any ALLOW or BLOCK user action should
// really clear PROMPT), this order seems safer than the other way around.
let value = TemporaryPermissions.get(browser, permissionID);
if (value) {
result.state = value.state;
result.scope = this.SCOPE_TEMPORARY;
}
}
return result;
},
/**
* Sets the state of a particular permission for a given principal or browser.
* This method will dispatch a "PermissionStateChange" event on the specified
* browser if a temporary permission was set
*
* @param {nsIPrincipal} [principal] The principal to set the permission for.
* When setting temporary permissions passing a principal is optional.
* If the principal is still passed here it takes precedence over the
* browser's contentPrincipal for permission keying. This can be
* helpful in situations where the browser has already navigated away
* from a site you want to set a permission for.
* @param {String} permissionID The id of the permission.
* @param {SitePermissions state} state The state of the permission.
* @param {SitePermissions scope} [scope] The scope of the permission.
* Defaults to SCOPE_PERSISTENT.
* @param {Browser} [browser] The browser object to set temporary permissions
* on. This needs to be provided if the scope is SCOPE_TEMPORARY!
* @param {number} [expireTimeMS] If setting a temporary permission, how many
* milliseconds it should be valid for. The default is controlled by
* the 'privacy.temporary_permission_expire_time_ms' pref.
*/
setForPrincipal(
principal,
permissionID,
state,
scope = this.SCOPE_PERSISTENT,
browser = null,
expireTimeMS = SitePermissions.temporaryPermissionExpireTime
) {
if (!principal && !browser) {
throw new Error(
"Atleast one of the arguments, either principal or browser should not be null."
);
}
if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
if (GloballyBlockedPermissions.set(browser, permissionID)) {
browser.dispatchEvent(
new browser.ownerGlobal.CustomEvent("PermissionStateChange")
);
}
return;
}
if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
// Because they are controlled by two prefs with many states that do not
// correspond to the classical ALLOW/DENY/PROMPT model, we want to always
// allow the user to add exceptions to their cookie rules without removing them.
if (permissionID != "cookie") {
this.removeFromPrincipal(principal, permissionID, browser);
return;
}
}
if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
throw new Error(
"ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission"
);
}
// Save temporary permissions.
if (scope == this.SCOPE_TEMPORARY) {
if (!browser) {
throw new Error(
"TEMPORARY scoped permissions require a browser object"
);
}
if (!Number.isInteger(expireTimeMS) || expireTimeMS <= 0) {
throw new Error("expireTime must be a positive integer");
}
if (
TemporaryPermissions.set(
browser,
permissionID,
state,
expireTimeMS,
principal ?? browser.contentPrincipal,
// On permission expiry
origBrowser => {
if (!origBrowser.ownerGlobal) {
return;
}
origBrowser.dispatchEvent(
new origBrowser.ownerGlobal.CustomEvent("PermissionStateChange")
);
}
)
) {
browser.dispatchEvent(
new browser.ownerGlobal.CustomEvent("PermissionStateChange")
);
}
} else if (this.isSupportedPrincipal(principal)) {
let perms_scope = Services.perms.EXPIRE_NEVER;
if (scope == this.SCOPE_SESSION) {
perms_scope = Services.perms.EXPIRE_SESSION;
} else if (scope == this.SCOPE_POLICY) {
perms_scope = Services.perms.EXPIRE_POLICY;
}
Services.perms.addFromPrincipal(
principal,
permissionID,
state,
perms_scope
);
}
},
/**
* Removes the saved state of a particular permission for a given principal and/or browser.
* This method will dispatch a "PermissionStateChange" event on the specified
* browser if a temporary permission was removed.
*
* @param {nsIPrincipal} principal
* The principal to remove the permission for.
* @param {String} permissionID
* The id of the permission.
* @param {Browser} browser (optional)
* The browser object to remove temporary permissions on.
*/
removeFromPrincipal(principal, permissionID, browser) {
if (!principal && !browser) {
throw new Error(
"Atleast one of the arguments, either principal or browser should not be null."
);
}
if (this.isSupportedPrincipal(principal)) {
Services.perms.removeFromPrincipal(principal, permissionID);
}
// TemporaryPermissions.get() deletes expired permissions automatically,
// if it hasn't expired, remove it explicitly.
if (TemporaryPermissions.remove(browser, permissionID)) {
// Send a PermissionStateChange event only if the permission hasn't expired.
browser.dispatchEvent(
new browser.ownerGlobal.CustomEvent("PermissionStateChange")
);
}
},
/**
* Clears all block permissions that were temporarily saved.
*
* @param {Browser} browser
* The browser object to clear.
*/
clearTemporaryBlockPermissions(browser) {
TemporaryPermissions.clear(browser, SitePermissions.BLOCK);
},
/**
* Copy all permissions that were temporarily saved on one
* browser object to a new browser.
*
* @param {Browser} browser
* The browser object to copy from.
* @param {Browser} newBrowser
* The browser object to copy to.
*/
copyTemporaryPermissions(browser, newBrowser) {
TemporaryPermissions.copy(browser, newBrowser);
GloballyBlockedPermissions.copy(browser, newBrowser);
},
/**
* Returns the localized label for the permission with the given ID, to be
* used in a UI for managing permissions.
* If a permission is double keyed (has an additional key in the ID), the
* second key is split off and supplied to the string formatter as a variable.
*
* @param {string} permissionID
* The permission to get the label for. May include second key.
*
* @return {String} the localized label or null if none is available.
*/
getPermissionLabel(permissionID) {
let [id, key] = permissionID.split(this.PERM_KEY_DELIMITER);
if (!gPermissions.has(id)) {
// Permission can't be found.
return null;
}
if (
"labelID" in gPermissions.get(id) &&
gPermissions.get(id).labelID === null
) {
// Permission doesn't support having a label.
return null;
}
if (id == "3rdPartyStorage" || id == "3rdPartyFrameStorage") {
// The key is the 3rd party origin or site, which we use for the label.
return key;
}
let labelID = gPermissions.get(id).labelID || id;
return gStringBundle.formatStringFromName(`permission.${labelID}.label`, [
key,
]);
},
/**
* Returns the localized label for the given permission state, to be used in
* a UI for managing permissions.
*
* @param {string} permissionID
* The permission to get the label for.
*
* @param {SitePermissions state} state
* The state to get the label for.
*
* @return {String|null} the localized label or null if an
* unknown state was passed.
*/
getMultichoiceStateLabel(permissionID, state) {
// If the permission has custom logic for getting its default value,
// try that first.
if (
gPermissions.has(permissionID) &&
gPermissions.get(permissionID).getMultichoiceStateLabel
) {
return gPermissions.get(permissionID).getMultichoiceStateLabel(state);
}
switch (state) {
case this.UNKNOWN:
case this.PROMPT:
return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk");
case this.ALLOW:
return gStringBundle.GetStringFromName("state.multichoice.allow");
case this.ALLOW_COOKIES_FOR_SESSION:
return gStringBundle.GetStringFromName(
"state.multichoice.allowForSession"
);
case this.BLOCK:
return gStringBundle.GetStringFromName("state.multichoice.block");
default:
return null;
}
},
/**
* Returns the localized label for a permission's current state.
*
* @param {SitePermissions state} state
* The state to get the label for.
* @param {string} id
* The permission to get the state label for.
* @param {SitePermissions scope} scope (optional)
* The scope to get the label for.
*
* @return {String|null} the localized label or null if an
* unknown state was passed.
*/
getCurrentStateLabel(state, id, scope = null) {
switch (state) {
case this.PROMPT:
return gStringBundle.GetStringFromName("state.current.prompt");
case this.ALLOW:
if (
scope &&
scope != this.SCOPE_PERSISTENT &&
scope != this.SCOPE_POLICY
) {
return gStringBundle.GetStringFromName(
"state.current.allowedTemporarily"
);
}
return gStringBundle.GetStringFromName("state.current.allowed");
case this.ALLOW_COOKIES_FOR_SESSION:
return gStringBundle.GetStringFromName(
"state.current.allowedForSession"
);
case this.BLOCK:
if (
scope &&
scope != this.SCOPE_PERSISTENT &&
scope != this.SCOPE_POLICY &&
scope != this.SCOPE_GLOBAL
) {
return gStringBundle.GetStringFromName(
"state.current.blockedTemporarily"
);
}
return gStringBundle.GetStringFromName("state.current.blocked");
default:
return null;
}
},
};
let gPermissions = {
_getId(type) {
// Split off second key (if it exists).
let [id] = type.split(SitePermissions.PERM_KEY_DELIMITER);
return id;
},
has(type) {
return this._getId(type) in this._permissions;
},
get(type) {
let id = this._getId(type);
let perm = this._permissions[id];
if (perm) {
perm.id = id;
}
return perm;
},
getEnabledPermissions() {
return Object.keys(this._permissions).filter(
id => !this._permissions[id].disabled
);
},
/* Holds permission ID => options pairs.
*
* Supported options:
*
* - exactHostMatch
* Allows sub domains to have their own permissions.
* Defaults to false.
*
* - getDefault
* Called to get the permission's default state.
* Defaults to UNKNOWN, indicating that the user will be asked each time
* a page asks for that permissions.
*
* - labelID
* Use the given ID instead of the permission name for looking up strings.
* e.g. "desktop-notification2" to use permission.desktop-notification2.label
*
* - states
* Array of permission states to be exposed to the user.
* Defaults to ALLOW, BLOCK and the default state (see getDefault).
*
* - getMultichoiceStateLabel
* Optional method to overwrite SitePermissions#getMultichoiceStateLabel with custom label logic.
*/
_permissions: {
"autoplay-media": {
exactHostMatch: true,
getDefault() {
let pref = Services.prefs.getIntPref(
"media.autoplay.default",
Ci.nsIAutoplay.BLOCKED
);
if (pref == Ci.nsIAutoplay.ALLOWED) {
return SitePermissions.ALLOW;
}
if (pref == Ci.nsIAutoplay.BLOCKED_ALL) {
return SitePermissions.AUTOPLAY_BLOCKED_ALL;
}
return SitePermissions.BLOCK;
},
setDefault(value) {
let prefValue = Ci.nsIAutoplay.BLOCKED;
if (value == SitePermissions.ALLOW) {
prefValue = Ci.nsIAutoplay.ALLOWED;
} else if (value == SitePermissions.AUTOPLAY_BLOCKED_ALL) {
prefValue = Ci.nsIAutoplay.BLOCKED_ALL;
}
Services.prefs.setIntPref("media.autoplay.default", prefValue);
},
labelID: "autoplay",
states: [
SitePermissions.ALLOW,
SitePermissions.BLOCK,
SitePermissions.AUTOPLAY_BLOCKED_ALL,
],
getMultichoiceStateLabel(state) {
switch (state) {
case SitePermissions.AUTOPLAY_BLOCKED_ALL:
return gStringBundle.GetStringFromName(
"state.multichoice.autoplayblockall"
);
case SitePermissions.BLOCK:
return gStringBundle.GetStringFromName(
"state.multichoice.autoplayblock"
);
case SitePermissions.ALLOW:
return gStringBundle.GetStringFromName(
"state.multichoice.autoplayallow"
);
}
throw new Error(`Unknown state: ${state}`);
},
},
cookie: {
states: [
SitePermissions.ALLOW,
SitePermissions.ALLOW_COOKIES_FOR_SESSION,
SitePermissions.BLOCK,
],
getDefault() {
if (
Services.cookies.getCookieBehavior(false) ==
Ci.nsICookieService.BEHAVIOR_REJECT
) {
return SitePermissions.BLOCK;
}
return SitePermissions.ALLOW;
},
},
"desktop-notification": {
exactHostMatch: true,
labelID: "desktop-notification3",
},
camera: {
exactHostMatch: true,
},
microphone: {
exactHostMatch: true,
},
screen: {
exactHostMatch: true,
states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
},
speaker: {
exactHostMatch: true,
states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
get disabled() {
return !SitePermissions.setSinkIdEnabled;
},
},
popup: {
getDefault() {
return Services.prefs.getBoolPref("dom.disable_open_during_load")
? SitePermissions.BLOCK
: SitePermissions.ALLOW;
},
states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
},
install: {
getDefault() {
return Services.prefs.getBoolPref("xpinstall.whitelist.required")
? SitePermissions.UNKNOWN
: SitePermissions.ALLOW;
},
},
geo: {
exactHostMatch: true,
},
"open-protocol-handler": {
labelID: "open-protocol-handler",
exactHostMatch: true,
states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
get disabled() {
return !SitePermissions.openProtoPermissionEnabled;
},
},
xr: {
exactHostMatch: true,
},
"focus-tab-by-prompt": {
exactHostMatch: true,
states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
},
"persistent-storage": {
exactHostMatch: true,
},
shortcuts: {
states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
},
canvas: {
get disabled() {
return !SitePermissions.resistFingerprinting;
},
},
midi: {
exactHostMatch: true,
get disabled() {
return !SitePermissions.midiPermissionEnabled;
},
},
"midi-sysex": {
exactHostMatch: true,
get disabled() {
return !SitePermissions.midiPermissionEnabled;
},
},
"storage-access": {
labelID: null,
getDefault() {
return SitePermissions.UNKNOWN;
},
},
"3rdPartyStorage": {},
"3rdPartyFrameStorage": {},
},
};
SitePermissions.midiPermissionEnabled = Services.prefs.getBoolPref(
"dom.webmidi.enabled"
);
XPCOMUtils.defineLazyPreferenceGetter(
SitePermissions,
"temporaryPermissionExpireTime",
"privacy.temporary_permission_expire_time_ms",
3600 * 1000
);
XPCOMUtils.defineLazyPreferenceGetter(
SitePermissions,
"setSinkIdEnabled",
"media.setsinkid.enabled",
false,
SitePermissions.invalidatePermissionList.bind(SitePermissions)
);
XPCOMUtils.defineLazyPreferenceGetter(
SitePermissions,
"resistFingerprinting",
"privacy.resistFingerprinting",
false,
SitePermissions.invalidatePermissionList.bind(SitePermissions)
);
XPCOMUtils.defineLazyPreferenceGetter(
SitePermissions,
"openProtoPermissionEnabled",
"security.external_protocol_requires_permission",
true,
SitePermissions.invalidatePermissionList.bind(SitePermissions)
);