1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
11 setTimeout: "resource://gre/modules/Timer.sys.mjs",
14 var gStringBundle = Services.strings.createBundle(
15 "chrome://browser/locale/sitePermissions.properties"
19 * A helper module to manage temporary permissions.
21 * Permissions are keyed by browser, so methods take a Browser
22 * element to identify the corresponding permission set.
24 * This uses a WeakMap to key browsers, so that entries are
25 * automatically cleared once the browser stops existing
26 * (once there are no other references to the browser object);
28 const TemporaryPermissions = {
29 // This is a three level deep map with the following structure:
32 // <baseDomain|origin>: {
33 // <permissionID>: {state: Number, expireTimeout: Number}
37 // Only the top level browser elements are stored via WeakMap. The WeakMap
38 // value is an object with URI baseDomains or origins as keys. The keys of
39 // that object are ids that identify permissions that were set for the
40 // specific URI. The final value is an object containing the permission state
41 // and the id of the timeout which will cause permission expiry.
42 // BLOCK permissions are keyed under baseDomain to prevent bypassing the block
43 // (see Bug 1492668). Any other permissions are keyed under origin.
44 _stateByBrowser: new WeakMap(),
46 // Extract baseDomain from uri. Fallback to hostname on conversion error.
47 _uriToBaseDomain(uri) {
49 return Services.eTLD.getBaseDomain(uri);
52 error.result !== Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
53 error.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
62 * Generate keys to store temporary permissions under. The strict key is
63 * origin, non-strict is baseDomain.
64 * @param {nsIPrincipal} principal - principal to derive keys from.
65 * @returns {Object} keys - Object containing the generated permission keys.
66 * @returns {string} keys.strict - Key to be used for strict matching.
67 * @returns {string} keys.nonStrict - Key to be used for non-strict matching.
68 * @throws {Error} - Throws if principal is undefined or no valid permission key can
71 _getKeysFromPrincipal(principal) {
72 return { strict: principal.origin, nonStrict: principal.baseDomain };
76 * Sets a new permission for the specified browser.
77 * @returns {boolean} whether the permission changed, effectively.
84 principal = browser.contentPrincipal,
90 !SitePermissions.isSupportedPrincipal(principal)
94 let entry = this._stateByBrowser.get(browser);
96 entry = { browser: Cu.getWeakReference(browser), uriToPerm: {} };
97 this._stateByBrowser.set(browser, entry);
99 let { uriToPerm } = entry;
100 // We store blocked permissions by baseDomain. Other states by origin.
101 let { strict, nonStrict } = this._getKeysFromPrincipal(principal);
104 // Differentiate between block and non-block permissions. If we store a
105 // block permission we need to delete old entries which may be set under
106 // origin before setting the new permission for baseDomain. For non-block
107 // permissions this is swapped.
108 if (state == SitePermissions.BLOCK) {
113 deleteKey = nonStrict;
116 if (!uriToPerm[setKey]) {
117 uriToPerm[setKey] = {};
120 let expireTimeout = uriToPerm[setKey][id]?.expireTimeout;
121 let previousState = uriToPerm[setKey][id]?.state;
122 // If overwriting a permission state. We need to cancel the old timeout.
124 lazy.clearTimeout(expireTimeout);
126 // Construct the new timeout to remove the permission once it has expired.
127 expireTimeout = lazy.setTimeout(() => {
128 let entryBrowser = entry.browser.get();
129 // Exit early if the browser is no longer alive when we get the timeout
131 if (!entryBrowser || !uriToPerm[setKey]) {
134 delete uriToPerm[setKey][id];
135 // Notify SitePermissions that a temporary permission has expired.
136 // Get the browser the permission is currently set for. If this.copy was
137 // used this browser is different from the original one passed above.
138 expireCallback(entryBrowser);
140 uriToPerm[setKey][id] = {
145 // If we set a permission state for a origin we need to reset the old state
146 // which may be set for baseDomain and vice versa. An individual permission
147 // must only ever be keyed by either origin or baseDomain.
148 let permissions = uriToPerm[deleteKey];
150 expireTimeout = permissions[id]?.expireTimeout;
152 lazy.clearTimeout(expireTimeout);
154 delete permissions[id];
157 return state != previousState;
161 * Removes a permission with the specified id for the specified browser.
162 * @returns {boolean} whether the permission was removed.
164 remove(browser, id) {
167 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
168 !this._stateByBrowser.has(browser)
172 // Permission can be stored by any of the two keys (strict and non-strict).
173 // getKeysFromURI can throw. We let the caller handle the exception.
174 let { strict, nonStrict } = this._getKeysFromPrincipal(
175 browser.contentPrincipal
177 let { uriToPerm } = this._stateByBrowser.get(browser);
178 for (let key of [nonStrict, strict]) {
179 if (uriToPerm[key]?.[id] != null) {
180 let { expireTimeout } = uriToPerm[key][id];
182 lazy.clearTimeout(expireTimeout);
184 delete uriToPerm[key][id];
185 // Individual permissions can only ever be keyed either strict or
186 // non-strict. If we find the permission via the first key run we can
194 // Gets a permission with the specified id for the specified browser.
198 !browser.contentPrincipal ||
199 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
200 !this._stateByBrowser.has(browser)
204 let { uriToPerm } = this._stateByBrowser.get(browser);
206 let { strict, nonStrict } = this._getKeysFromPrincipal(
207 browser.contentPrincipal
209 for (let key of [nonStrict, strict]) {
210 if (uriToPerm[key]) {
211 let permission = uriToPerm[key][id];
215 state: permission.state,
216 scope: SitePermissions.SCOPE_TEMPORARY,
224 // Gets all permissions for the specified browser.
225 // Note that only permissions that apply to the current URI
226 // of the passed browser element will be returned.
228 let permissions = [];
230 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
231 !this._stateByBrowser.has(browser)
235 let { uriToPerm } = this._stateByBrowser.get(browser);
237 let { strict, nonStrict } = this._getKeysFromPrincipal(
238 browser.contentPrincipal
240 for (let key of [nonStrict, strict]) {
241 if (uriToPerm[key]) {
242 let perms = uriToPerm[key];
243 for (let id of Object.keys(perms)) {
244 let permission = perms[id];
248 state: permission.state,
249 scope: SitePermissions.SCOPE_TEMPORARY,
259 // Clears all permissions for the specified browser.
260 // Unlike other methods, this does NOT clear only for
261 // the currentURI but the whole browser state.
264 * Clear temporary permissions for the specified browser. Unlike other
265 * methods, this does NOT clear only for the currentURI but the whole browser
267 * @param {Browser} browser - Browser to clear permissions for.
268 * @param {Number} [filterState] - Only clear permissions with the given state
269 * value. Defaults to all permissions.
271 clear(browser, filterState = null) {
272 let entry = this._stateByBrowser.get(browser);
273 if (!entry?.uriToPerm) {
277 let { uriToPerm } = entry;
278 Object.entries(uriToPerm).forEach(([uriKey, permissions]) => {
279 Object.entries(permissions).forEach(
280 ([permId, { state, expireTimeout }]) => {
281 // We need to explicitly check for null or undefined here, because the
282 // permission state may be 0.
283 if (filterState != null) {
284 if (state != filterState) {
285 // Skip permission entry if it doesn't match the filter.
288 delete permissions[permId];
290 // For the clear-all case we remove the entire browser entry, so we
291 // only need to clear the timeouts.
292 if (!expireTimeout) {
295 lazy.clearTimeout(expireTimeout);
298 // If there are no more permissions, remove the entry from the URI map.
299 if (filterState != null && !Object.keys(permissions).length) {
300 delete uriToPerm[uriKey];
304 // We're either clearing all permissions or only the permissions with state
305 // == filterState. If we have a filter, we can only clean up the browser if
306 // there are no permission entries left in the map.
307 if (filterState == null || !Object.keys(uriToPerm).length) {
308 this._stateByBrowser.delete(browser);
312 // Copies the temporary permission state of one browser
313 // into a new entry for the other browser.
314 copy(browser, newBrowser) {
315 let entry = this._stateByBrowser.get(browser);
317 entry.browser = Cu.getWeakReference(newBrowser);
318 this._stateByBrowser.set(newBrowser, entry);
323 // This hold a flag per browser to indicate whether we should show the
324 // user a notification as a permission has been requested that has been
325 // blocked globally. We only want to notify the user in the case that
326 // they actually requested the permission within the current page load
327 // so will clear the flag on navigation.
328 const GloballyBlockedPermissions = {
329 _stateByBrowser: new WeakMap(),
332 * @returns {boolean} whether the permission was removed.
335 if (!this._stateByBrowser.has(browser)) {
336 this._stateByBrowser.set(browser, {});
338 let entry = this._stateByBrowser.get(browser);
339 let origin = browser.contentPrincipal.origin;
340 if (!entry[origin]) {
344 if (entry[origin][id]) {
347 entry[origin][id] = true;
349 // Clear the flag and remove the listener once the user has navigated.
350 // WebProgress will report various things including hashchanges to us, the
351 // navigation we care about is either leaving the current page or reloading.
352 let { prePath } = browser.currentURI;
353 browser.addProgressListener(
355 QueryInterface: ChromeUtils.generateQI([
356 "nsIWebProgressListener",
357 "nsISupportsWeakReference",
359 onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
361 aLocation.prePath != prePath ||
362 !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
364 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
367 if (aWebProgress.isTopLevel && (hasLeftPage || isReload)) {
368 GloballyBlockedPermissions.remove(browser, id, origin);
369 browser.removeProgressListener(this);
373 Ci.nsIWebProgress.NOTIFY_LOCATION
378 // Removes a permission with the specified id for the specified browser.
379 remove(browser, id, origin = null) {
380 let entry = this._stateByBrowser.get(browser);
382 origin = browser.contentPrincipal.origin;
384 if (entry && entry[origin]) {
385 delete entry[origin][id];
389 // Gets all permissions for the specified browser.
390 // Note that only permissions that apply to the current URI
391 // of the passed browser element will be returned.
393 let permissions = [];
394 let entry = this._stateByBrowser.get(browser);
395 let origin = browser.contentPrincipal.origin;
396 if (entry && entry[origin]) {
397 let timeStamps = entry[origin];
398 for (let id of Object.keys(timeStamps)) {
401 state: gPermissions.get(id).getDefault(),
402 scope: SitePermissions.SCOPE_GLOBAL,
409 // Copies the globally blocked permission state of one browser
410 // into a new entry for the other browser.
411 copy(browser, newBrowser) {
412 let entry = this._stateByBrowser.get(browser);
414 this._stateByBrowser.set(newBrowser, entry);
420 * A module to manage permanent and temporary permissions
421 * by URI and browser.
423 * Some methods have the side effect of dispatching a "PermissionStateChange"
424 * event on changes to temporary permissions, as mentioned in the respective docs.
426 export var SitePermissions = {
427 // Permission states.
428 UNKNOWN: Services.perms.UNKNOWN_ACTION,
429 ALLOW: Services.perms.ALLOW_ACTION,
430 BLOCK: Services.perms.DENY_ACTION,
431 PROMPT: Services.perms.PROMPT_ACTION,
432 ALLOW_COOKIES_FOR_SESSION: Ci.nsICookiePermission.ACCESS_SESSION,
433 AUTOPLAY_BLOCKED_ALL: Ci.nsIAutoplay.BLOCKED_ALL,
435 // Permission scopes.
436 SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}",
437 SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}",
438 SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
439 SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
440 SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
441 SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
443 // The delimiter used for double keyed permissions.
444 // For example: open-protocol-handler^irc
445 PERM_KEY_DELIMITER: "^",
447 _permissionsArray: null,
448 _defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
450 // For testing use only.
451 _temporaryPermissions: TemporaryPermissions,
454 * Gets all custom permissions for a given principal.
455 * Install addon permission is excluded, check bug 1303108.
457 * @return {Array} a list of objects with the keys:
458 * - id: the permissionId of the permission
459 * - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
460 * - state: a constant representing the current permission state
461 * (e.g. SitePermissions.ALLOW)
463 getAllByPrincipal(principal) {
465 throw new Error("principal argument cannot be null.");
467 if (!this.isSupportedPrincipal(principal)) {
471 // Get all permissions from the permission manager by principal, excluding
472 // the ones set to be disabled.
473 let permissions = Services.perms
474 .getAllForPrincipal(principal)
475 .filter(permission => {
476 let entry = gPermissions.get(permission.type);
477 if (!entry || entry.disabled) {
482 /* Hide persistent storage permission when extension principal
483 * have WebExtensions-unlimitedStorage permission. */
485 type == "persistent-storage" &&
486 SitePermissions.getForPrincipal(
488 "WebExtensions-unlimitedStorage"
489 ).state == SitePermissions.ALLOW
497 return permissions.map(permission => {
498 let scope = this.SCOPE_PERSISTENT;
499 if (permission.expireType == Services.perms.EXPIRE_SESSION) {
500 scope = this.SCOPE_SESSION;
501 } else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
502 scope = this.SCOPE_POLICY;
508 state: permission.capability,
514 * Returns all custom permissions for a given browser.
516 * To receive a more detailed, albeit less performant listing see
517 * SitePermissions.getAllPermissionDetailsForBrowser().
519 * @param {Browser} browser
520 * The browser to fetch permission for.
522 * @return {Array} a list of objects with the keys:
523 * - id: the permissionId of the permission
524 * - state: a constant representing the current permission state
525 * (e.g. SitePermissions.ALLOW)
526 * - scope: a constant representing how long the permission will
529 getAllForBrowser(browser) {
530 let permissions = {};
532 for (let permission of TemporaryPermissions.getAll(browser)) {
533 permission.scope = this.SCOPE_TEMPORARY;
534 permissions[permission.id] = permission;
537 for (let permission of GloballyBlockedPermissions.getAll(browser)) {
538 permissions[permission.id] = permission;
541 for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) {
542 permissions[permission.id] = permission;
545 return Object.values(permissions);
549 * Returns a list of objects with detailed information on all permissions
550 * that are currently set for the given browser.
552 * @param {Browser} browser
553 * The browser to fetch permission for.
555 * @return {Array<Object>} a list of objects with the keys:
556 * - id: the permissionID of the permission
557 * - state: a constant representing the current permission state
558 * (e.g. SitePermissions.ALLOW)
559 * - scope: a constant representing how long the permission will
561 * - label: the localized label, or null if none is available.
563 getAllPermissionDetailsForBrowser(browser) {
564 return this.getAllForBrowser(browser).map(({ id, scope, state }) => ({
568 label: this.getPermissionLabel(id),
573 * Checks whether a UI for managing permissions should be exposed for a given
576 * @param {nsIPrincipal} principal
577 * The principal to check.
579 * @return {boolean} if the principal is supported.
581 isSupportedPrincipal(principal) {
585 if (!(principal instanceof Ci.nsIPrincipal)) {
587 "Argument passed as principal is not an instance of Ci.nsIPrincipal"
590 return this.isSupportedScheme(principal.scheme);
594 * Checks whether we support managing permissions for a specific scheme.
595 * @param {string} scheme - Scheme to test.
596 * @returns {boolean} Whether the scheme is supported.
598 isSupportedScheme(scheme) {
599 return ["http", "https", "moz-extension", "file"].includes(scheme);
603 * Gets an array of all permission IDs.
605 * @return {Array<String>} an array of all permission IDs.
608 if (this._permissionsArray === null) {
609 this._permissionsArray = gPermissions.getEnabledPermissions();
611 return this._permissionsArray;
615 * Test whether a permission is managed by SitePermissions.
616 * @param {string} type - Permission type.
619 isSitePermission(type) {
620 return gPermissions.has(type);
624 * Called when a preference changes its value.
626 * @param {string} data
627 * The last argument passed to the preference change observer
628 * @param {string} previous
629 * The previous value of the preference
630 * @param {string} latest
631 * The latest value of the preference
633 invalidatePermissionList(data, previous, latest) {
634 // Ensure that listPermissions() will reconstruct its return value the next
636 this._permissionsArray = null;
640 * Returns an array of permission states to be exposed to the user for a
641 * permission with the given ID.
643 * @param {string} permissionID
644 * The ID to get permission states for.
646 * @return {Array<SitePermissions state>} an array of all permission states.
648 getAvailableStates(permissionID) {
650 gPermissions.has(permissionID) &&
651 gPermissions.get(permissionID).states
653 return gPermissions.get(permissionID).states;
656 /* Since the permissions we are dealing with have adopted the convention
657 * of treating UNKNOWN == PROMPT, we only include one of either UNKNOWN
658 * or PROMPT in this list, to avoid duplicating states. */
659 if (this.getDefault(permissionID) == this.UNKNOWN) {
661 SitePermissions.UNKNOWN,
662 SitePermissions.ALLOW,
663 SitePermissions.BLOCK,
668 SitePermissions.PROMPT,
669 SitePermissions.ALLOW,
670 SitePermissions.BLOCK,
675 * Returns the default state of a particular permission.
677 * @param {string} permissionID
678 * The ID to get the default for.
680 * @return {SitePermissions.state} the default state.
682 getDefault(permissionID) {
683 // If the permission has custom logic for getting its default value,
686 gPermissions.has(permissionID) &&
687 gPermissions.get(permissionID).getDefault
689 return gPermissions.get(permissionID).getDefault();
692 // Otherwise try to get the default preference for that permission.
693 return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
697 * Set the default state of a particular permission.
699 * @param {string} permissionID
700 * The ID to set the default for.
702 * @param {string} state
705 setDefault(permissionID, state) {
707 gPermissions.has(permissionID) &&
708 gPermissions.get(permissionID).setDefault
710 return gPermissions.get(permissionID).setDefault(state);
712 let key = "permissions.default." + permissionID;
713 return Services.prefs.setIntPref(key, state);
717 * Returns the state and scope of a particular permission for a given principal.
719 * This method will NOT dispatch a "PermissionStateChange" event on the specified
720 * browser if a temporary permission was removed because it has expired.
722 * @param {nsIPrincipal} principal
723 * The principal to check.
724 * @param {String} permissionID
725 * The id of the permission.
726 * @param {Browser} [browser] The browser object to check for temporary
729 * @return {Object} an object with the keys:
730 * - state: The current state of the permission
731 * (e.g. SitePermissions.ALLOW)
732 * - scope: The scope of the permission
733 * (e.g. SitePermissions.SCOPE_PERSISTENT)
735 getForPrincipal(principal, permissionID, browser) {
736 if (!principal && !browser) {
738 "Atleast one of the arguments, either principal or browser should not be null."
741 let defaultState = this.getDefault(permissionID);
742 let result = { state: defaultState, scope: this.SCOPE_PERSISTENT };
743 if (this.isSupportedPrincipal(principal)) {
744 let permission = null;
746 gPermissions.has(permissionID) &&
747 gPermissions.get(permissionID).exactHostMatch
749 permission = Services.perms.getPermissionObject(
755 permission = Services.perms.getPermissionObject(
763 result.state = permission.capability;
764 if (permission.expireType == Services.perms.EXPIRE_SESSION) {
765 result.scope = this.SCOPE_SESSION;
766 } else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
767 result.scope = this.SCOPE_POLICY;
772 if (result.state == defaultState) {
773 // If there's no persistent permission saved, check if we have something
775 let value = TemporaryPermissions.get(browser, permissionID);
778 result.state = value.state;
779 result.scope = this.SCOPE_TEMPORARY;
787 * Sets the state of a particular permission for a given principal or browser.
788 * This method will dispatch a "PermissionStateChange" event on the specified
789 * browser if a temporary permission was set
791 * @param {nsIPrincipal} [principal] The principal to set the permission for.
792 * When setting temporary permissions passing a principal is optional.
793 * If the principal is still passed here it takes precedence over the
794 * browser's contentPrincipal for permission keying. This can be
795 * helpful in situations where the browser has already navigated away
796 * from a site you want to set a permission for.
797 * @param {String} permissionID The id of the permission.
798 * @param {SitePermissions state} state The state of the permission.
799 * @param {SitePermissions scope} [scope] The scope of the permission.
800 * Defaults to SCOPE_PERSISTENT.
801 * @param {Browser} [browser] The browser object to set temporary permissions
802 * on. This needs to be provided if the scope is SCOPE_TEMPORARY!
803 * @param {number} [expireTimeMS] If setting a temporary permission, how many
804 * milliseconds it should be valid for. The default is controlled by
805 * the 'privacy.temporary_permission_expire_time_ms' pref.
811 scope = this.SCOPE_PERSISTENT,
813 expireTimeMS = SitePermissions.temporaryPermissionExpireTime
815 if (!principal && !browser) {
817 "Atleast one of the arguments, either principal or browser should not be null."
820 if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
821 if (GloballyBlockedPermissions.set(browser, permissionID)) {
822 browser.dispatchEvent(
823 new browser.ownerGlobal.CustomEvent("PermissionStateChange")
829 if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
830 // Because they are controlled by two prefs with many states that do not
831 // correspond to the classical ALLOW/DENY/PROMPT model, we want to always
832 // allow the user to add exceptions to their cookie rules without removing them.
833 if (permissionID != "cookie") {
834 this.removeFromPrincipal(principal, permissionID, browser);
839 if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
841 "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission"
845 // Save temporary permissions.
846 if (scope == this.SCOPE_TEMPORARY) {
849 "TEMPORARY scoped permissions require a browser object"
852 if (!Number.isInteger(expireTimeMS) || expireTimeMS <= 0) {
853 throw new Error("expireTime must be a positive integer");
857 TemporaryPermissions.set(
862 principal ?? browser.contentPrincipal,
863 // On permission expiry
865 if (!origBrowser.ownerGlobal) {
868 origBrowser.dispatchEvent(
869 new origBrowser.ownerGlobal.CustomEvent("PermissionStateChange")
874 browser.dispatchEvent(
875 new browser.ownerGlobal.CustomEvent("PermissionStateChange")
878 } else if (this.isSupportedPrincipal(principal)) {
879 let perms_scope = Services.perms.EXPIRE_NEVER;
880 if (scope == this.SCOPE_SESSION) {
881 perms_scope = Services.perms.EXPIRE_SESSION;
882 } else if (scope == this.SCOPE_POLICY) {
883 perms_scope = Services.perms.EXPIRE_POLICY;
886 Services.perms.addFromPrincipal(
896 * Removes the saved state of a particular permission for a given principal and/or browser.
897 * This method will dispatch a "PermissionStateChange" event on the specified
898 * browser if a temporary permission was removed.
900 * @param {nsIPrincipal} principal
901 * The principal to remove the permission for.
902 * @param {String} permissionID
903 * The id of the permission.
904 * @param {Browser} browser (optional)
905 * The browser object to remove temporary permissions on.
907 removeFromPrincipal(principal, permissionID, browser) {
908 if (!principal && !browser) {
910 "Atleast one of the arguments, either principal or browser should not be null."
913 if (this.isSupportedPrincipal(principal)) {
914 Services.perms.removeFromPrincipal(principal, permissionID);
917 // TemporaryPermissions.get() deletes expired permissions automatically,
918 // if it hasn't expired, remove it explicitly.
919 if (TemporaryPermissions.remove(browser, permissionID)) {
920 // Send a PermissionStateChange event only if the permission hasn't expired.
921 browser.dispatchEvent(
922 new browser.ownerGlobal.CustomEvent("PermissionStateChange")
928 * Clears all block permissions that were temporarily saved.
930 * @param {Browser} browser
931 * The browser object to clear.
933 clearTemporaryBlockPermissions(browser) {
934 TemporaryPermissions.clear(browser, SitePermissions.BLOCK);
938 * Copy all permissions that were temporarily saved on one
939 * browser object to a new browser.
941 * @param {Browser} browser
942 * The browser object to copy from.
943 * @param {Browser} newBrowser
944 * The browser object to copy to.
946 copyTemporaryPermissions(browser, newBrowser) {
947 TemporaryPermissions.copy(browser, newBrowser);
948 GloballyBlockedPermissions.copy(browser, newBrowser);
952 * Returns the localized label for the permission with the given ID, to be
953 * used in a UI for managing permissions.
954 * If a permission is double keyed (has an additional key in the ID), the
955 * second key is split off and supplied to the string formatter as a variable.
957 * @param {string} permissionID
958 * The permission to get the label for. May include second key.
960 * @return {String} the localized label or null if none is available.
962 getPermissionLabel(permissionID) {
963 let [id, key] = permissionID.split(this.PERM_KEY_DELIMITER);
964 if (!gPermissions.has(id)) {
965 // Permission can't be found.
969 "labelID" in gPermissions.get(id) &&
970 gPermissions.get(id).labelID === null
972 // Permission doesn't support having a label.
975 if (id == "3rdPartyStorage" || id == "3rdPartyFrameStorage") {
976 // The key is the 3rd party origin or site, which we use for the label.
979 let labelID = gPermissions.get(id).labelID || id;
980 return gStringBundle.formatStringFromName(`permission.${labelID}.label`, [
986 * Returns the localized label for the given permission state, to be used in
987 * a UI for managing permissions.
989 * @param {string} permissionID
990 * The permission to get the label for.
992 * @param {SitePermissions state} state
993 * The state to get the label for.
995 * @return {String|null} the localized label or null if an
996 * unknown state was passed.
998 getMultichoiceStateLabel(permissionID, state) {
999 // If the permission has custom logic for getting its default value,
1002 gPermissions.has(permissionID) &&
1003 gPermissions.get(permissionID).getMultichoiceStateLabel
1005 return gPermissions.get(permissionID).getMultichoiceStateLabel(state);
1011 return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk");
1013 return gStringBundle.GetStringFromName("state.multichoice.allow");
1014 case this.ALLOW_COOKIES_FOR_SESSION:
1015 return gStringBundle.GetStringFromName(
1016 "state.multichoice.allowForSession"
1019 return gStringBundle.GetStringFromName("state.multichoice.block");
1026 * Returns the localized label for a permission's current state.
1028 * @param {SitePermissions state} state
1029 * The state to get the label for.
1030 * @param {string} id
1031 * The permission to get the state label for.
1032 * @param {SitePermissions scope} scope (optional)
1033 * The scope to get the label for.
1035 * @return {String|null} the localized label or null if an
1036 * unknown state was passed.
1038 getCurrentStateLabel(state, id, scope = null) {
1041 return gStringBundle.GetStringFromName("state.current.prompt");
1045 scope != this.SCOPE_PERSISTENT &&
1046 scope != this.SCOPE_POLICY
1048 return gStringBundle.GetStringFromName(
1049 "state.current.allowedTemporarily"
1052 return gStringBundle.GetStringFromName("state.current.allowed");
1053 case this.ALLOW_COOKIES_FOR_SESSION:
1054 return gStringBundle.GetStringFromName(
1055 "state.current.allowedForSession"
1060 scope != this.SCOPE_PERSISTENT &&
1061 scope != this.SCOPE_POLICY &&
1062 scope != this.SCOPE_GLOBAL
1064 return gStringBundle.GetStringFromName(
1065 "state.current.blockedTemporarily"
1068 return gStringBundle.GetStringFromName("state.current.blocked");
1075 let gPermissions = {
1077 // Split off second key (if it exists).
1078 let [id] = type.split(SitePermissions.PERM_KEY_DELIMITER);
1083 return this._getId(type) in this._permissions;
1087 let id = this._getId(type);
1088 let perm = this._permissions[id];
1095 getEnabledPermissions() {
1096 return Object.keys(this._permissions).filter(
1097 id => !this._permissions[id].disabled
1101 /* Holds permission ID => options pairs.
1103 * Supported options:
1106 * Allows sub domains to have their own permissions.
1107 * Defaults to false.
1110 * Called to get the permission's default state.
1111 * Defaults to UNKNOWN, indicating that the user will be asked each time
1112 * a page asks for that permissions.
1115 * Use the given ID instead of the permission name for looking up strings.
1116 * e.g. "desktop-notification2" to use permission.desktop-notification2.label
1119 * Array of permission states to be exposed to the user.
1120 * Defaults to ALLOW, BLOCK and the default state (see getDefault).
1122 * - getMultichoiceStateLabel
1123 * Optional method to overwrite SitePermissions#getMultichoiceStateLabel with custom label logic.
1127 exactHostMatch: true,
1129 let pref = Services.prefs.getIntPref(
1130 "media.autoplay.default",
1131 Ci.nsIAutoplay.BLOCKED
1133 if (pref == Ci.nsIAutoplay.ALLOWED) {
1134 return SitePermissions.ALLOW;
1136 if (pref == Ci.nsIAutoplay.BLOCKED_ALL) {
1137 return SitePermissions.AUTOPLAY_BLOCKED_ALL;
1139 return SitePermissions.BLOCK;
1142 let prefValue = Ci.nsIAutoplay.BLOCKED;
1143 if (value == SitePermissions.ALLOW) {
1144 prefValue = Ci.nsIAutoplay.ALLOWED;
1145 } else if (value == SitePermissions.AUTOPLAY_BLOCKED_ALL) {
1146 prefValue = Ci.nsIAutoplay.BLOCKED_ALL;
1148 Services.prefs.setIntPref("media.autoplay.default", prefValue);
1150 labelID: "autoplay",
1152 SitePermissions.ALLOW,
1153 SitePermissions.BLOCK,
1154 SitePermissions.AUTOPLAY_BLOCKED_ALL,
1156 getMultichoiceStateLabel(state) {
1158 case SitePermissions.AUTOPLAY_BLOCKED_ALL:
1159 return gStringBundle.GetStringFromName(
1160 "state.multichoice.autoplayblockall"
1162 case SitePermissions.BLOCK:
1163 return gStringBundle.GetStringFromName(
1164 "state.multichoice.autoplayblock"
1166 case SitePermissions.ALLOW:
1167 return gStringBundle.GetStringFromName(
1168 "state.multichoice.autoplayallow"
1171 throw new Error(`Unknown state: ${state}`);
1177 SitePermissions.ALLOW,
1178 SitePermissions.ALLOW_COOKIES_FOR_SESSION,
1179 SitePermissions.BLOCK,
1183 Services.cookies.getCookieBehavior(false) ==
1184 Ci.nsICookieService.BEHAVIOR_REJECT
1186 return SitePermissions.BLOCK;
1189 return SitePermissions.ALLOW;
1193 "desktop-notification": {
1194 exactHostMatch: true,
1195 labelID: "desktop-notification3",
1199 exactHostMatch: true,
1203 exactHostMatch: true,
1207 exactHostMatch: true,
1208 states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
1212 exactHostMatch: true,
1213 states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
1215 return !SitePermissions.setSinkIdEnabled;
1221 return Services.prefs.getBoolPref("dom.disable_open_during_load")
1222 ? SitePermissions.BLOCK
1223 : SitePermissions.ALLOW;
1225 states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1230 return Services.prefs.getBoolPref("xpinstall.whitelist.required")
1231 ? SitePermissions.UNKNOWN
1232 : SitePermissions.ALLOW;
1237 exactHostMatch: true,
1240 "open-protocol-handler": {
1241 labelID: "open-protocol-handler",
1242 exactHostMatch: true,
1243 states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1245 return !SitePermissions.openProtoPermissionEnabled;
1250 exactHostMatch: true,
1253 "focus-tab-by-prompt": {
1254 exactHostMatch: true,
1255 states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1257 "persistent-storage": {
1258 exactHostMatch: true,
1262 states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1267 return !SitePermissions.resistFingerprinting;
1272 exactHostMatch: true,
1274 return !SitePermissions.midiPermissionEnabled;
1279 exactHostMatch: true,
1281 return !SitePermissions.midiPermissionEnabled;
1288 return SitePermissions.UNKNOWN;
1292 "3rdPartyStorage": {},
1293 "3rdPartyFrameStorage": {},
1297 SitePermissions.midiPermissionEnabled = Services.prefs.getBoolPref(
1298 "dom.webmidi.enabled"
1301 XPCOMUtils.defineLazyPreferenceGetter(
1303 "temporaryPermissionExpireTime",
1304 "privacy.temporary_permission_expire_time_ms",
1307 XPCOMUtils.defineLazyPreferenceGetter(
1310 "media.setsinkid.enabled",
1312 SitePermissions.invalidatePermissionList.bind(SitePermissions)
1314 XPCOMUtils.defineLazyPreferenceGetter(
1316 "resistFingerprinting",
1317 "privacy.resistFingerprinting",
1319 SitePermissions.invalidatePermissionList.bind(SitePermissions)
1321 XPCOMUtils.defineLazyPreferenceGetter(
1323 "openProtoPermissionEnabled",
1324 "security.external_protocol_requires_permission",
1326 SitePermissions.invalidatePermissionList.bind(SitePermissions)