/* 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 => { // : { // : {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} 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} 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(data, previous, latest) { // 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} 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) { // If there's no persistent permission saved, check if we have something // set temporarily. 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) );