Bug 1889091 - Part 3: Specialize calls to native functions with variadic parameters...
[gecko.git] / browser / modules / SitePermissions.sys.mjs
blob8e1aa7787141fbcdbb24a73050f318a14667cd24
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";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
11   setTimeout: "resource://gre/modules/Timer.sys.mjs",
12 });
14 var gStringBundle = Services.strings.createBundle(
15   "chrome://browser/locale/sitePermissions.properties"
18 /**
19  * A helper module to manage temporary permissions.
20  *
21  * Permissions are keyed by browser, so methods take a Browser
22  * element to identify the corresponding permission set.
23  *
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);
27  */
28 const TemporaryPermissions = {
29   // This is a three level deep map with the following structure:
30   //
31   // Browser => {
32   //   <baseDomain|origin>: {
33   //     <permissionID>: {state: Number, expireTimeout: Number}
34   //   }
35   // }
36   //
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) {
48     try {
49       return Services.eTLD.getBaseDomain(uri);
50     } catch (error) {
51       if (
52         error.result !== Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
53         error.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
54       ) {
55         throw error;
56       }
57       return uri.host;
58     }
59   },
61   /**
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
69    * be generated.
70    */
71   _getKeysFromPrincipal(principal) {
72     return { strict: principal.origin, nonStrict: principal.baseDomain };
73   },
75   /**
76    * Sets a new permission for the specified browser.
77    * @returns {boolean} whether the permission changed, effectively.
78    */
79   set(
80     browser,
81     id,
82     state,
83     expireTimeMS,
84     principal = browser.contentPrincipal,
85     expireCallback
86   ) {
87     if (
88       !browser ||
89       !principal ||
90       !SitePermissions.isSupportedPrincipal(principal)
91     ) {
92       return false;
93     }
94     let entry = this._stateByBrowser.get(browser);
95     if (!entry) {
96       entry = { browser: Cu.getWeakReference(browser), uriToPerm: {} };
97       this._stateByBrowser.set(browser, entry);
98     }
99     let { uriToPerm } = entry;
100     // We store blocked permissions by baseDomain. Other states by origin.
101     let { strict, nonStrict } = this._getKeysFromPrincipal(principal);
102     let setKey;
103     let deleteKey;
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) {
109       setKey = nonStrict;
110       deleteKey = strict;
111     } else {
112       setKey = strict;
113       deleteKey = nonStrict;
114     }
116     if (!uriToPerm[setKey]) {
117       uriToPerm[setKey] = {};
118     }
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.
123     if (expireTimeout) {
124       lazy.clearTimeout(expireTimeout);
125     }
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
130       // callback.
131       if (!entryBrowser || !uriToPerm[setKey]) {
132         return;
133       }
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);
139     }, expireTimeMS);
140     uriToPerm[setKey][id] = {
141       expireTimeout,
142       state,
143     };
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];
149     if (permissions) {
150       expireTimeout = permissions[id]?.expireTimeout;
151       if (expireTimeout) {
152         lazy.clearTimeout(expireTimeout);
153       }
154       delete permissions[id];
155     }
157     return state != previousState;
158   },
160   /**
161    * Removes a permission with the specified id for the specified browser.
162    * @returns {boolean} whether the permission was removed.
163    */
164   remove(browser, id) {
165     if (
166       !browser ||
167       !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
168       !this._stateByBrowser.has(browser)
169     ) {
170       return false;
171     }
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
176     );
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];
181         if (expireTimeout) {
182           lazy.clearTimeout(expireTimeout);
183         }
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
187         // return early.
188         return true;
189       }
190     }
191     return false;
192   },
194   // Gets a permission with the specified id for the specified browser.
195   get(browser, id) {
196     if (
197       !browser ||
198       !browser.contentPrincipal ||
199       !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
200       !this._stateByBrowser.has(browser)
201     ) {
202       return null;
203     }
204     let { uriToPerm } = this._stateByBrowser.get(browser);
206     let { strict, nonStrict } = this._getKeysFromPrincipal(
207       browser.contentPrincipal
208     );
209     for (let key of [nonStrict, strict]) {
210       if (uriToPerm[key]) {
211         let permission = uriToPerm[key][id];
212         if (permission) {
213           return {
214             id,
215             state: permission.state,
216             scope: SitePermissions.SCOPE_TEMPORARY,
217           };
218         }
219       }
220     }
221     return null;
222   },
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.
227   getAll(browser) {
228     let permissions = [];
229     if (
230       !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) ||
231       !this._stateByBrowser.has(browser)
232     ) {
233       return permissions;
234     }
235     let { uriToPerm } = this._stateByBrowser.get(browser);
237     let { strict, nonStrict } = this._getKeysFromPrincipal(
238       browser.contentPrincipal
239     );
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];
245           if (permission) {
246             permissions.push({
247               id,
248               state: permission.state,
249               scope: SitePermissions.SCOPE_TEMPORARY,
250             });
251           }
252         }
253       }
254     }
256     return permissions;
257   },
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.
263   /**
264    * Clear temporary permissions for the specified browser. Unlike other
265    * methods, this does NOT clear only for the currentURI but the whole browser
266    * state.
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.
270    */
271   clear(browser, filterState = null) {
272     let entry = this._stateByBrowser.get(browser);
273     if (!entry?.uriToPerm) {
274       return;
275     }
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.
286               return;
287             }
288             delete permissions[permId];
289           }
290           // For the clear-all case we remove the entire browser entry, so we
291           // only need to clear the timeouts.
292           if (!expireTimeout) {
293             return;
294           }
295           lazy.clearTimeout(expireTimeout);
296         }
297       );
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];
301       }
302     });
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);
309     }
310   },
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);
316     if (entry) {
317       entry.browser = Cu.getWeakReference(newBrowser);
318       this._stateByBrowser.set(newBrowser, entry);
319     }
320   },
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(),
331   /**
332    * @returns {boolean} whether the permission was removed.
333    */
334   set(browser, id) {
335     if (!this._stateByBrowser.has(browser)) {
336       this._stateByBrowser.set(browser, {});
337     }
338     let entry = this._stateByBrowser.get(browser);
339     let origin = browser.contentPrincipal.origin;
340     if (!entry[origin]) {
341       entry[origin] = {};
342     }
344     if (entry[origin][id]) {
345       return false;
346     }
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(
354       {
355         QueryInterface: ChromeUtils.generateQI([
356           "nsIWebProgressListener",
357           "nsISupportsWeakReference",
358         ]),
359         onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
360           let hasLeftPage =
361             aLocation.prePath != prePath ||
362             !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
363           let isReload = !!(
364             aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
365           );
367           if (aWebProgress.isTopLevel && (hasLeftPage || isReload)) {
368             GloballyBlockedPermissions.remove(browser, id, origin);
369             browser.removeProgressListener(this);
370           }
371         },
372       },
373       Ci.nsIWebProgress.NOTIFY_LOCATION
374     );
375     return true;
376   },
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);
381     if (!origin) {
382       origin = browser.contentPrincipal.origin;
383     }
384     if (entry && entry[origin]) {
385       delete entry[origin][id];
386     }
387   },
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.
392   getAll(browser) {
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)) {
399         permissions.push({
400           id,
401           state: gPermissions.get(id).getDefault(),
402           scope: SitePermissions.SCOPE_GLOBAL,
403         });
404       }
405     }
406     return permissions;
407   },
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);
413     if (entry) {
414       this._stateByBrowser.set(newBrowser, entry);
415     }
416   },
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.
425  */
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,
453   /**
454    * Gets all custom permissions for a given principal.
455    * Install addon permission is excluded, check bug 1303108.
456    *
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)
462    */
463   getAllByPrincipal(principal) {
464     if (!principal) {
465       throw new Error("principal argument cannot be null.");
466     }
467     if (!this.isSupportedPrincipal(principal)) {
468       return [];
469     }
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) {
478           return false;
479         }
480         let type = entry.id;
482         /* Hide persistent storage permission when extension principal
483          * have WebExtensions-unlimitedStorage permission. */
484         if (
485           type == "persistent-storage" &&
486           SitePermissions.getForPrincipal(
487             principal,
488             "WebExtensions-unlimitedStorage"
489           ).state == SitePermissions.ALLOW
490         ) {
491           return false;
492         }
494         return true;
495       });
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;
503       }
505       return {
506         id: permission.type,
507         scope,
508         state: permission.capability,
509       };
510     });
511   },
513   /**
514    * Returns all custom permissions for a given browser.
515    *
516    * To receive a more detailed, albeit less performant listing see
517    * SitePermissions.getAllPermissionDetailsForBrowser().
518    *
519    * @param {Browser} browser
520    *        The browser to fetch permission for.
521    *
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
527    *           be kept.
528    */
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;
535     }
537     for (let permission of GloballyBlockedPermissions.getAll(browser)) {
538       permissions[permission.id] = permission;
539     }
541     for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) {
542       permissions[permission.id] = permission;
543     }
545     return Object.values(permissions);
546   },
548   /**
549    * Returns a list of objects with detailed information on all permissions
550    * that are currently set for the given browser.
551    *
552    * @param {Browser} browser
553    *        The browser to fetch permission for.
554    *
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
560    *             be kept.
561    *           - label: the localized label, or null if none is available.
562    */
563   getAllPermissionDetailsForBrowser(browser) {
564     return this.getAllForBrowser(browser).map(({ id, scope, state }) => ({
565       id,
566       scope,
567       state,
568       label: this.getPermissionLabel(id),
569     }));
570   },
572   /**
573    * Checks whether a UI for managing permissions should be exposed for a given
574    * principal.
575    *
576    * @param {nsIPrincipal} principal
577    *        The principal to check.
578    *
579    * @return {boolean} if the principal is supported.
580    */
581   isSupportedPrincipal(principal) {
582     if (!principal) {
583       return false;
584     }
585     if (!(principal instanceof Ci.nsIPrincipal)) {
586       throw new Error(
587         "Argument passed as principal is not an instance of Ci.nsIPrincipal"
588       );
589     }
590     return this.isSupportedScheme(principal.scheme);
591   },
593   /**
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.
597    */
598   isSupportedScheme(scheme) {
599     return ["http", "https", "moz-extension", "file"].includes(scheme);
600   },
602   /**
603    * Gets an array of all permission IDs.
604    *
605    * @return {Array<String>} an array of all permission IDs.
606    */
607   listPermissions() {
608     if (this._permissionsArray === null) {
609       this._permissionsArray = gPermissions.getEnabledPermissions();
610     }
611     return this._permissionsArray;
612   },
614   /**
615    * Test whether a permission is managed by SitePermissions.
616    * @param {string} type - Permission type.
617    * @returns {boolean}
618    */
619   isSitePermission(type) {
620     return gPermissions.has(type);
621   },
623   /**
624    * Called when a preference changes its value.
625    *
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
632    */
633   invalidatePermissionList() {
634     // Ensure that listPermissions() will reconstruct its return value the next
635     // time it's called.
636     this._permissionsArray = null;
637   },
639   /**
640    * Returns an array of permission states to be exposed to the user for a
641    * permission with the given ID.
642    *
643    * @param {string} permissionID
644    *        The ID to get permission states for.
645    *
646    * @return {Array<SitePermissions state>} an array of all permission states.
647    */
648   getAvailableStates(permissionID) {
649     if (
650       gPermissions.has(permissionID) &&
651       gPermissions.get(permissionID).states
652     ) {
653       return gPermissions.get(permissionID).states;
654     }
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) {
660       return [
661         SitePermissions.UNKNOWN,
662         SitePermissions.ALLOW,
663         SitePermissions.BLOCK,
664       ];
665     }
667     return [
668       SitePermissions.PROMPT,
669       SitePermissions.ALLOW,
670       SitePermissions.BLOCK,
671     ];
672   },
674   /**
675    * Returns the default state of a particular permission.
676    *
677    * @param {string} permissionID
678    *        The ID to get the default for.
679    *
680    * @return {SitePermissions.state} the default state.
681    */
682   getDefault(permissionID) {
683     // If the permission has custom logic for getting its default value,
684     // try that first.
685     if (
686       gPermissions.has(permissionID) &&
687       gPermissions.get(permissionID).getDefault
688     ) {
689       return gPermissions.get(permissionID).getDefault();
690     }
692     // Otherwise try to get the default preference for that permission.
693     return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
694   },
696   /**
697    * Set the default state of a particular permission.
698    *
699    * @param {string} permissionID
700    *        The ID to set the default for.
701    *
702    * @param {string} state
703    *        The state to set.
704    */
705   setDefault(permissionID, state) {
706     if (
707       gPermissions.has(permissionID) &&
708       gPermissions.get(permissionID).setDefault
709     ) {
710       return gPermissions.get(permissionID).setDefault(state);
711     }
712     let key = "permissions.default." + permissionID;
713     return Services.prefs.setIntPref(key, state);
714   },
716   /**
717    * Returns the state and scope of a particular permission for a given principal.
718    *
719    * This method will NOT dispatch a "PermissionStateChange" event on the specified
720    * browser if a temporary permission was removed because it has expired.
721    *
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
727    *        permissions.
728    *
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)
734    */
735   getForPrincipal(principal, permissionID, browser) {
736     if (!principal && !browser) {
737       throw new Error(
738         "Atleast one of the arguments, either principal or browser should not be null."
739       );
740     }
741     let defaultState = this.getDefault(permissionID);
742     let result = { state: defaultState, scope: this.SCOPE_PERSISTENT };
743     if (this.isSupportedPrincipal(principal)) {
744       let permission = null;
745       if (
746         gPermissions.has(permissionID) &&
747         gPermissions.get(permissionID).exactHostMatch
748       ) {
749         permission = Services.perms.getPermissionObject(
750           principal,
751           permissionID,
752           true
753         );
754       } else {
755         permission = Services.perms.getPermissionObject(
756           principal,
757           permissionID,
758           false
759         );
760       }
762       if (permission) {
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;
768         }
769       }
770     }
772     if (result.state == defaultState) {
773       // If there's no persistent permission saved, check if we have something
774       // set temporarily.
775       let value = TemporaryPermissions.get(browser, permissionID);
777       if (value) {
778         result.state = value.state;
779         result.scope = this.SCOPE_TEMPORARY;
780       }
781     }
783     return result;
784   },
786   /**
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
790    *
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.
806    */
807   setForPrincipal(
808     principal,
809     permissionID,
810     state,
811     scope = this.SCOPE_PERSISTENT,
812     browser = null,
813     expireTimeMS = SitePermissions.temporaryPermissionExpireTime
814   ) {
815     if (!principal && !browser) {
816       throw new Error(
817         "Atleast one of the arguments, either principal or browser should not be null."
818       );
819     }
820     if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
821       if (GloballyBlockedPermissions.set(browser, permissionID)) {
822         browser.dispatchEvent(
823           new browser.ownerGlobal.CustomEvent("PermissionStateChange")
824         );
825       }
826       return;
827     }
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);
835         return;
836       }
837     }
839     if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
840       throw new Error(
841         "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission"
842       );
843     }
845     // Save temporary permissions.
846     if (scope == this.SCOPE_TEMPORARY) {
847       if (!browser) {
848         throw new Error(
849           "TEMPORARY scoped permissions require a browser object"
850         );
851       }
852       if (!Number.isInteger(expireTimeMS) || expireTimeMS <= 0) {
853         throw new Error("expireTime must be a positive integer");
854       }
856       if (
857         TemporaryPermissions.set(
858           browser,
859           permissionID,
860           state,
861           expireTimeMS,
862           principal ?? browser.contentPrincipal,
863           // On permission expiry
864           origBrowser => {
865             if (!origBrowser.ownerGlobal) {
866               return;
867             }
868             origBrowser.dispatchEvent(
869               new origBrowser.ownerGlobal.CustomEvent("PermissionStateChange")
870             );
871           }
872         )
873       ) {
874         browser.dispatchEvent(
875           new browser.ownerGlobal.CustomEvent("PermissionStateChange")
876         );
877       }
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;
884       }
886       Services.perms.addFromPrincipal(
887         principal,
888         permissionID,
889         state,
890         perms_scope
891       );
892     }
893   },
895   /**
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.
899    *
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.
906    */
907   removeFromPrincipal(principal, permissionID, browser) {
908     if (!principal && !browser) {
909       throw new Error(
910         "Atleast one of the arguments, either principal or browser should not be null."
911       );
912     }
913     if (this.isSupportedPrincipal(principal)) {
914       Services.perms.removeFromPrincipal(principal, permissionID);
915     }
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")
923       );
924     }
925   },
927   /**
928    * Clears all block permissions that were temporarily saved.
929    *
930    * @param {Browser} browser
931    *        The browser object to clear.
932    */
933   clearTemporaryBlockPermissions(browser) {
934     TemporaryPermissions.clear(browser, SitePermissions.BLOCK);
935   },
937   /**
938    * Copy all permissions that were temporarily saved on one
939    * browser object to a new browser.
940    *
941    * @param {Browser} browser
942    *        The browser object to copy from.
943    * @param {Browser} newBrowser
944    *        The browser object to copy to.
945    */
946   copyTemporaryPermissions(browser, newBrowser) {
947     TemporaryPermissions.copy(browser, newBrowser);
948     GloballyBlockedPermissions.copy(browser, newBrowser);
949   },
951   /**
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.
956    *
957    * @param {string} permissionID
958    *        The permission to get the label for. May include second key.
959    *
960    * @return {String} the localized label or null if none is available.
961    */
962   getPermissionLabel(permissionID) {
963     let [id, key] = permissionID.split(this.PERM_KEY_DELIMITER);
964     if (!gPermissions.has(id)) {
965       // Permission can't be found.
966       return null;
967     }
968     if (
969       "labelID" in gPermissions.get(id) &&
970       gPermissions.get(id).labelID === null
971     ) {
972       // Permission doesn't support having a label.
973       return null;
974     }
975     if (id == "3rdPartyStorage" || id == "3rdPartyFrameStorage") {
976       // The key is the 3rd party origin or site, which we use for the label.
977       return key;
978     }
979     let labelID = gPermissions.get(id).labelID || id;
980     return gStringBundle.formatStringFromName(`permission.${labelID}.label`, [
981       key,
982     ]);
983   },
985   /**
986    * Returns the localized label for the given permission state, to be used in
987    * a UI for managing permissions.
988    *
989    * @param {string} permissionID
990    *        The permission to get the label for.
991    *
992    * @param {SitePermissions state} state
993    *        The state to get the label for.
994    *
995    * @return {String|null} the localized label or null if an
996    *         unknown state was passed.
997    */
998   getMultichoiceStateLabel(permissionID, state) {
999     // If the permission has custom logic for getting its default value,
1000     // try that first.
1001     if (
1002       gPermissions.has(permissionID) &&
1003       gPermissions.get(permissionID).getMultichoiceStateLabel
1004     ) {
1005       return gPermissions.get(permissionID).getMultichoiceStateLabel(state);
1006     }
1008     switch (state) {
1009       case this.UNKNOWN:
1010       case this.PROMPT:
1011         return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk");
1012       case this.ALLOW:
1013         return gStringBundle.GetStringFromName("state.multichoice.allow");
1014       case this.ALLOW_COOKIES_FOR_SESSION:
1015         return gStringBundle.GetStringFromName(
1016           "state.multichoice.allowForSession"
1017         );
1018       case this.BLOCK:
1019         return gStringBundle.GetStringFromName("state.multichoice.block");
1020       default:
1021         return null;
1022     }
1023   },
1025   /**
1026    * Returns the localized label for a permission's current state.
1027    *
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.
1034    *
1035    * @return {String|null} the localized label or null if an
1036    *         unknown state was passed.
1037    */
1038   getCurrentStateLabel(state, id, scope = null) {
1039     switch (state) {
1040       case this.PROMPT:
1041         return gStringBundle.GetStringFromName("state.current.prompt");
1042       case this.ALLOW:
1043         if (
1044           scope &&
1045           scope != this.SCOPE_PERSISTENT &&
1046           scope != this.SCOPE_POLICY
1047         ) {
1048           return gStringBundle.GetStringFromName(
1049             "state.current.allowedTemporarily"
1050           );
1051         }
1052         return gStringBundle.GetStringFromName("state.current.allowed");
1053       case this.ALLOW_COOKIES_FOR_SESSION:
1054         return gStringBundle.GetStringFromName(
1055           "state.current.allowedForSession"
1056         );
1057       case this.BLOCK:
1058         if (
1059           scope &&
1060           scope != this.SCOPE_PERSISTENT &&
1061           scope != this.SCOPE_POLICY &&
1062           scope != this.SCOPE_GLOBAL
1063         ) {
1064           return gStringBundle.GetStringFromName(
1065             "state.current.blockedTemporarily"
1066           );
1067         }
1068         return gStringBundle.GetStringFromName("state.current.blocked");
1069       default:
1070         return null;
1071     }
1072   },
1075 let gPermissions = {
1076   _getId(type) {
1077     // Split off second key (if it exists).
1078     let [id] = type.split(SitePermissions.PERM_KEY_DELIMITER);
1079     return id;
1080   },
1082   has(type) {
1083     return this._getId(type) in this._permissions;
1084   },
1086   get(type) {
1087     let id = this._getId(type);
1088     let perm = this._permissions[id];
1089     if (perm) {
1090       perm.id = id;
1091     }
1092     return perm;
1093   },
1095   getEnabledPermissions() {
1096     return Object.keys(this._permissions).filter(
1097       id => !this._permissions[id].disabled
1098     );
1099   },
1101   /* Holds permission ID => options pairs.
1102    *
1103    * Supported options:
1104    *
1105    *  - exactHostMatch
1106    *    Allows sub domains to have their own permissions.
1107    *    Defaults to false.
1108    *
1109    *  - getDefault
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.
1113    *
1114    *  - labelID
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
1117    *
1118    *  - states
1119    *    Array of permission states to be exposed to the user.
1120    *    Defaults to ALLOW, BLOCK and the default state (see getDefault).
1121    *
1122    *  - getMultichoiceStateLabel
1123    *    Optional method to overwrite SitePermissions#getMultichoiceStateLabel with custom label logic.
1124    */
1125   _permissions: {
1126     "autoplay-media": {
1127       exactHostMatch: true,
1128       getDefault() {
1129         let pref = Services.prefs.getIntPref(
1130           "media.autoplay.default",
1131           Ci.nsIAutoplay.BLOCKED
1132         );
1133         if (pref == Ci.nsIAutoplay.ALLOWED) {
1134           return SitePermissions.ALLOW;
1135         }
1136         if (pref == Ci.nsIAutoplay.BLOCKED_ALL) {
1137           return SitePermissions.AUTOPLAY_BLOCKED_ALL;
1138         }
1139         return SitePermissions.BLOCK;
1140       },
1141       setDefault(value) {
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;
1147         }
1148         Services.prefs.setIntPref("media.autoplay.default", prefValue);
1149       },
1150       labelID: "autoplay",
1151       states: [
1152         SitePermissions.ALLOW,
1153         SitePermissions.BLOCK,
1154         SitePermissions.AUTOPLAY_BLOCKED_ALL,
1155       ],
1156       getMultichoiceStateLabel(state) {
1157         switch (state) {
1158           case SitePermissions.AUTOPLAY_BLOCKED_ALL:
1159             return gStringBundle.GetStringFromName(
1160               "state.multichoice.autoplayblockall"
1161             );
1162           case SitePermissions.BLOCK:
1163             return gStringBundle.GetStringFromName(
1164               "state.multichoice.autoplayblock"
1165             );
1166           case SitePermissions.ALLOW:
1167             return gStringBundle.GetStringFromName(
1168               "state.multichoice.autoplayallow"
1169             );
1170         }
1171         throw new Error(`Unknown state: ${state}`);
1172       },
1173     },
1175     cookie: {
1176       states: [
1177         SitePermissions.ALLOW,
1178         SitePermissions.ALLOW_COOKIES_FOR_SESSION,
1179         SitePermissions.BLOCK,
1180       ],
1181       getDefault() {
1182         if (
1183           Services.cookies.getCookieBehavior(false) ==
1184           Ci.nsICookieService.BEHAVIOR_REJECT
1185         ) {
1186           return SitePermissions.BLOCK;
1187         }
1189         return SitePermissions.ALLOW;
1190       },
1191     },
1193     "desktop-notification": {
1194       exactHostMatch: true,
1195       labelID: "desktop-notification3",
1196     },
1198     camera: {
1199       exactHostMatch: true,
1200     },
1202     microphone: {
1203       exactHostMatch: true,
1204     },
1206     screen: {
1207       exactHostMatch: true,
1208       states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
1209     },
1211     speaker: {
1212       exactHostMatch: true,
1213       states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
1214       get disabled() {
1215         return !SitePermissions.setSinkIdEnabled;
1216       },
1217     },
1219     popup: {
1220       getDefault() {
1221         return Services.prefs.getBoolPref("dom.disable_open_during_load")
1222           ? SitePermissions.BLOCK
1223           : SitePermissions.ALLOW;
1224       },
1225       states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1226     },
1228     install: {
1229       getDefault() {
1230         return Services.prefs.getBoolPref("xpinstall.whitelist.required")
1231           ? SitePermissions.UNKNOWN
1232           : SitePermissions.ALLOW;
1233       },
1234     },
1236     geo: {
1237       exactHostMatch: true,
1238     },
1240     "open-protocol-handler": {
1241       labelID: "open-protocol-handler",
1242       exactHostMatch: true,
1243       states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1244       get disabled() {
1245         return !SitePermissions.openProtoPermissionEnabled;
1246       },
1247     },
1249     xr: {
1250       exactHostMatch: true,
1251     },
1253     "focus-tab-by-prompt": {
1254       exactHostMatch: true,
1255       states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1256     },
1257     "persistent-storage": {
1258       exactHostMatch: true,
1259     },
1261     shortcuts: {
1262       states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1263     },
1265     canvas: {
1266       get disabled() {
1267         return !SitePermissions.resistFingerprinting;
1268       },
1269     },
1271     midi: {
1272       exactHostMatch: true,
1273       get disabled() {
1274         return !SitePermissions.midiPermissionEnabled;
1275       },
1276     },
1278     "midi-sysex": {
1279       exactHostMatch: true,
1280       get disabled() {
1281         return !SitePermissions.midiPermissionEnabled;
1282       },
1283     },
1285     "storage-access": {
1286       labelID: null,
1287       getDefault() {
1288         return SitePermissions.UNKNOWN;
1289       },
1290     },
1292     "3rdPartyStorage": {},
1293     "3rdPartyFrameStorage": {},
1294   },
1297 SitePermissions.midiPermissionEnabled = Services.prefs.getBoolPref(
1298   "dom.webmidi.enabled"
1301 XPCOMUtils.defineLazyPreferenceGetter(
1302   SitePermissions,
1303   "temporaryPermissionExpireTime",
1304   "privacy.temporary_permission_expire_time_ms",
1305   3600 * 1000
1307 XPCOMUtils.defineLazyPreferenceGetter(
1308   SitePermissions,
1309   "setSinkIdEnabled",
1310   "media.setsinkid.enabled",
1311   false,
1312   SitePermissions.invalidatePermissionList.bind(SitePermissions)
1314 XPCOMUtils.defineLazyPreferenceGetter(
1315   SitePermissions,
1316   "resistFingerprinting",
1317   "privacy.resistFingerprinting",
1318   false,
1319   SitePermissions.invalidatePermissionList.bind(SitePermissions)
1321 XPCOMUtils.defineLazyPreferenceGetter(
1322   SitePermissions,
1323   "openProtoPermissionEnabled",
1324   "security.external_protocol_requires_permission",
1325   true,
1326   SitePermissions.invalidatePermissionList.bind(SitePermissions)