Bug 1675375 Part 7: Update expectations in helper_hittest_clippath.html. r=botond
[gecko.git] / browser / modules / SitePermissions.jsm
blob6716db45a76c817ae9b2bde53c5db182e2dec1c7
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 var EXPORTED_SYMBOLS = ["SitePermissions"];
7 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const { XPCOMUtils } = ChromeUtils.import(
9   "resource://gre/modules/XPCOMUtils.jsm"
12 XPCOMUtils.defineLazyModuleGetters(this, {
13   clearTimeout: "resource://gre/modules/Timer.jsm",
14   setTimeout: "resource://gre/modules/Timer.jsm",
15 });
17 var gStringBundle = Services.strings.createBundle(
18   "chrome://browser/locale/sitePermissions.properties"
21 /**
22  * A helper module to manage temporary permissions.
23  *
24  * Permissions are keyed by browser, so methods take a Browser
25  * element to identify the corresponding permission set.
26  *
27  * This uses a WeakMap to key browsers, so that entries are
28  * automatically cleared once the browser stops existing
29  * (once there are no other references to the browser object);
30  */
31 const TemporaryPermissions = {
32   // This is a three level deep map with the following structure:
33   //
34   // Browser => {
35   //   <baseDomain|prePath>: {
36   //     <permissionID>: {state: Number, expireTimeout: Number}
37   //   }
38   // }
39   //
40   // Only the top level browser elements are stored via WeakMap. The WeakMap
41   // value is an object with URI baseDomains or prePaths as keys. The keys of
42   // that object are ids that identify permissions that were set for the
43   // specific URI. The final value is an object containing the permission state
44   // and the id of the timeout which will cause permission expiry.
45   // BLOCK permissions are keyed under baseDomain to prevent bypassing the block
46   // (see Bug 1492668). Any other permissions are keyed under URI prePath.
47   _stateByBrowser: new WeakMap(),
49   // Extract baseDomain from uri. Fallback to hostname on conversion error.
50   _uriToBaseDomain(uri) {
51     try {
52       return Services.eTLD.getBaseDomain(uri);
53     } catch (error) {
54       if (
55         error.result !== Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
56         error.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
57       ) {
58         throw error;
59       }
60       return uri.host;
61     }
62   },
64   /**
65    * Generate keys to store temporary permissions under. The strict key is
66    * nsIURI.prePath, non-strict is URI baseDomain.
67    * @param {nsIURI} uri - URI to derive keys from.
68    * @returns {Object} keys - Object containing the generated permission keys.
69    * @returns {string} keys.strict - Key to be used for strict matching.
70    * @returns {string} keys.nonStrict - Key to be used for non-strict matching.
71    * @throws {Error} - Throws if URI is undefined or no valid permission key can
72    * be generated.
73    */
74   _getKeysFromURI(uri) {
75     return { strict: uri.prePath, nonStrict: this._uriToBaseDomain(uri) };
76   },
78   // Sets a new permission for the specified browser.
79   set(browser, id, state, expireTimeMS, browserURI, expireCallback) {
80     if (
81       !browser ||
82       !browserURI ||
83       !SitePermissions.isSupportedScheme(browserURI.scheme)
84     ) {
85       return;
86     }
87     let entry = this._stateByBrowser.get(browser);
88     if (!entry) {
89       entry = { browser: Cu.getWeakReference(browser), uriToPerm: {} };
90       this._stateByBrowser.set(browser, entry);
91     }
92     let { uriToPerm } = entry;
93     // We store blocked permissions by baseDomain. Other states by URI prePath.
94     let { strict, nonStrict } = this._getKeysFromURI(browserURI);
95     let setKey;
96     let deleteKey;
97     // Differenciate between block and non-block permissions. If we store a
98     // block permission we need to delete old entries which may be set under URI
99     // prePath before setting the new permission for baseDomain. For non-block
100     // permissions this is swapped.
101     if (state == SitePermissions.BLOCK) {
102       setKey = nonStrict;
103       deleteKey = strict;
104     } else {
105       setKey = strict;
106       deleteKey = nonStrict;
107     }
109     if (!uriToPerm[setKey]) {
110       uriToPerm[setKey] = {};
111     }
113     let expireTimeout = uriToPerm[setKey][id]?.expireTimeout;
114     // If overwriting a permission state. We need to cancel the old timeout.
115     if (expireTimeout) {
116       clearTimeout(expireTimeout);
117     }
118     // Construct the new timeout to remove the permission once it has expired.
119     expireTimeout = setTimeout(() => {
120       let entryBrowser = entry.browser.get();
121       // Exit early if the browser is no longer alive when we get the timeout
122       // callback.
123       if (!entryBrowser || !uriToPerm[setKey]) {
124         return;
125       }
126       delete uriToPerm[setKey][id];
127       // Notify SitePermissions that a temporary permission has expired.
128       // Get the browser the permission is currently set for. If this.copy was
129       // used this browser is different from the original one passed above.
130       expireCallback(entryBrowser);
131     }, expireTimeMS);
132     uriToPerm[setKey][id] = {
133       expireTimeout,
134       state,
135     };
137     // If we set a permission state for a prePath we need to reset the old state
138     // which may be set for baseDomain and vice versa. An individual permission
139     // must only ever be keyed by either prePath or baseDomain.
140     let permissions = uriToPerm[deleteKey];
141     if (!permissions) {
142       return;
143     }
144     expireTimeout = permissions[id]?.expireTimeout;
145     if (expireTimeout) {
146       clearTimeout(expireTimeout);
147     }
148     delete permissions[id];
149   },
151   // Removes a permission with the specified id for the specified browser.
152   remove(browser, id) {
153     if (
154       !browser ||
155       !SitePermissions.isSupportedScheme(browser.currentURI.scheme) ||
156       !this._stateByBrowser.has(browser)
157     ) {
158       return;
159     }
160     // Permission can be stored by any of the two keys (strict and non-strict).
161     // getKeysFromURI can throw. We let the caller handle the exception.
162     let { strict, nonStrict } = this._getKeysFromURI(browser.currentURI);
163     let { uriToPerm } = this._stateByBrowser.get(browser);
164     for (let key of [nonStrict, strict]) {
165       if (uriToPerm[key]?.[id] != null) {
166         let { expireTimeout } = uriToPerm[key][id];
167         if (expireTimeout) {
168           clearTimeout(expireTimeout);
169         }
170         delete uriToPerm[key][id];
171         // Individual permissions can only ever be keyed either strict or
172         // non-strict. If we find the permission via the first key run we can
173         // return early.
174         return;
175       }
176     }
177   },
179   // Gets a permission with the specified id for the specified browser.
180   get(browser, id) {
181     if (
182       !browser ||
183       !browser.currentURI ||
184       !SitePermissions.isSupportedScheme(browser.currentURI.scheme) ||
185       !this._stateByBrowser.has(browser)
186     ) {
187       return null;
188     }
189     let { uriToPerm } = this._stateByBrowser.get(browser);
191     let { strict, nonStrict } = this._getKeysFromURI(browser.currentURI);
192     for (let key of [nonStrict, strict]) {
193       if (uriToPerm[key]) {
194         let permission = uriToPerm[key][id];
195         if (permission) {
196           return {
197             id,
198             state: permission.state,
199             scope: SitePermissions.SCOPE_TEMPORARY,
200           };
201         }
202       }
203     }
204     return null;
205   },
207   // Gets all permissions for the specified browser.
208   // Note that only permissions that apply to the current URI
209   // of the passed browser element will be returned.
210   getAll(browser) {
211     let permissions = [];
212     if (
213       !SitePermissions.isSupportedScheme(browser.currentURI.scheme) ||
214       !this._stateByBrowser.has(browser)
215     ) {
216       return permissions;
217     }
218     let { uriToPerm } = this._stateByBrowser.get(browser);
220     let { strict, nonStrict } = this._getKeysFromURI(browser.currentURI);
221     for (let key of [nonStrict, strict]) {
222       if (uriToPerm[key]) {
223         let perms = uriToPerm[key];
224         for (let id of Object.keys(perms)) {
225           let permission = perms[id];
226           if (permission) {
227             permissions.push({
228               id,
229               state: permission.state,
230               scope: SitePermissions.SCOPE_TEMPORARY,
231             });
232           }
233         }
234       }
235     }
237     return permissions;
238   },
240   // Clears all permissions for the specified browser.
241   // Unlike other methods, this does NOT clear only for
242   // the currentURI but the whole browser state.
244   /**
245    * Clear temporary permissions for the specified browser. Unlike other
246    * methods, this does NOT clear only for the currentURI but the whole browser
247    * state.
248    * @param {Browser} browser - Browser to clear permissions for.
249    * @param {Number} [filterState] - Only clear permissions with the given state
250    * value. Defaults to all permissions.
251    */
252   clear(browser, filterState = null) {
253     let entry = this._stateByBrowser.get(browser);
254     if (!entry?.uriToPerm) {
255       return;
256     }
258     let { uriToPerm } = entry;
259     Object.entries(uriToPerm).forEach(([uriKey, permissions]) => {
260       Object.entries(permissions).forEach(
261         ([permId, { state, expireTimeout }]) => {
262           // We need to explicitly check for null or undefined here, because the
263           // permission state may be 0.
264           if (filterState != null) {
265             if (state != filterState) {
266               // Skip permission entry if it doesn't match the filter.
267               return;
268             }
269             delete permissions[permId];
270           }
271           // For the clear-all case we remove the entire browser entry, so we
272           // only need to clear the timeouts.
273           if (!expireTimeout) {
274             return;
275           }
276           clearTimeout(expireTimeout);
277         }
278       );
279       // If there are no more permissions, remove the entry from the URI map.
280       if (filterState != null && !Object.keys(permissions).length) {
281         delete uriToPerm[uriKey];
282       }
283     });
285     // We're either clearing all permissions or only the permissions with state
286     // == filterState. If we have a filter, we can only clean up the browser if
287     // there are no permission entries left in the map.
288     if (filterState == null || !Object.keys(uriToPerm).length) {
289       this._stateByBrowser.delete(browser);
290     }
291   },
293   // Copies the temporary permission state of one browser
294   // into a new entry for the other browser.
295   copy(browser, newBrowser) {
296     let entry = this._stateByBrowser.get(browser);
297     if (entry) {
298       entry.browser = Cu.getWeakReference(newBrowser);
299       this._stateByBrowser.set(newBrowser, entry);
300     }
301   },
304 // This hold a flag per browser to indicate whether we should show the
305 // user a notification as a permission has been requested that has been
306 // blocked globally. We only want to notify the user in the case that
307 // they actually requested the permission within the current page load
308 // so will clear the flag on navigation.
309 const GloballyBlockedPermissions = {
310   _stateByBrowser: new WeakMap(),
312   set(browser, id) {
313     if (!this._stateByBrowser.has(browser)) {
314       this._stateByBrowser.set(browser, {});
315     }
316     let entry = this._stateByBrowser.get(browser);
317     let prePath = browser.currentURI.prePath;
318     if (!entry[prePath]) {
319       entry[prePath] = {};
320     }
322     if (entry[prePath][id]) {
323       return;
324     }
325     entry[prePath][id] = true;
327     // Clear the flag and remove the listener once the user has navigated.
328     // WebProgress will report various things including hashchanges to us, the
329     // navigation we care about is either leaving the current page or reloading.
330     browser.addProgressListener(
331       {
332         QueryInterface: ChromeUtils.generateQI([
333           "nsIWebProgressListener",
334           "nsISupportsWeakReference",
335         ]),
336         onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
337           let hasLeftPage =
338             aLocation.prePath != prePath ||
339             !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
340           let isReload = !!(
341             aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
342           );
344           if (aWebProgress.isTopLevel && (hasLeftPage || isReload)) {
345             GloballyBlockedPermissions.remove(browser, id, prePath);
346             browser.removeProgressListener(this);
347           }
348         },
349       },
350       Ci.nsIWebProgress.NOTIFY_LOCATION
351     );
352   },
354   // Removes a permission with the specified id for the specified browser.
355   remove(browser, id, prePath = null) {
356     let entry = this._stateByBrowser.get(browser);
357     if (!prePath) {
358       prePath = browser.currentURI.prePath;
359     }
360     if (entry && entry[prePath]) {
361       delete entry[prePath][id];
362     }
363   },
365   // Gets all permissions for the specified browser.
366   // Note that only permissions that apply to the current URI
367   // of the passed browser element will be returned.
368   getAll(browser) {
369     let permissions = [];
370     let entry = this._stateByBrowser.get(browser);
371     let prePath = browser.currentURI.prePath;
372     if (entry && entry[prePath]) {
373       let timeStamps = entry[prePath];
374       for (let id of Object.keys(timeStamps)) {
375         permissions.push({
376           id,
377           state: gPermissions.get(id).getDefault(),
378           scope: SitePermissions.SCOPE_GLOBAL,
379         });
380       }
381     }
382     return permissions;
383   },
385   // Copies the globally blocked permission state of one browser
386   // into a new entry for the other browser.
387   copy(browser, newBrowser) {
388     let entry = this._stateByBrowser.get(browser);
389     if (entry) {
390       this._stateByBrowser.set(newBrowser, entry);
391     }
392   },
396  * A module to manage permanent and temporary permissions
397  * by URI and browser.
399  * Some methods have the side effect of dispatching a "PermissionStateChange"
400  * event on changes to temporary permissions, as mentioned in the respective docs.
401  */
402 var SitePermissions = {
403   // Permission states.
404   UNKNOWN: Services.perms.UNKNOWN_ACTION,
405   ALLOW: Services.perms.ALLOW_ACTION,
406   BLOCK: Services.perms.DENY_ACTION,
407   PROMPT: Services.perms.PROMPT_ACTION,
408   ALLOW_COOKIES_FOR_SESSION: Ci.nsICookiePermission.ACCESS_SESSION,
409   AUTOPLAY_BLOCKED_ALL: Ci.nsIAutoplay.BLOCKED_ALL,
411   // Permission scopes.
412   SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}",
413   SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}",
414   SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
415   SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
416   SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
417   SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
419   // The delimiter used for double keyed permissions.
420   // For example: open-protocol-handler^irc
421   PERM_KEY_DELIMITER: "^",
423   _permissionsArray: null,
424   _defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
426   // For testing use only.
427   _temporaryPermissions: TemporaryPermissions,
429   /**
430    * Gets all custom permissions for a given principal.
431    * Install addon permission is excluded, check bug 1303108.
432    *
433    * @return {Array} a list of objects with the keys:
434    *          - id: the permissionId of the permission
435    *          - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
436    *          - state: a constant representing the current permission state
437    *            (e.g. SitePermissions.ALLOW)
438    */
439   getAllByPrincipal(principal) {
440     if (!principal) {
441       throw new Error("principal argument cannot be null.");
442     }
443     if (!this.isSupportedPrincipal(principal)) {
444       return [];
445     }
447     // Get all permissions from the permission manager by principal, excluding
448     // the ones set to be disabled.
449     let permissions = Services.perms
450       .getAllForPrincipal(principal)
451       .filter(permission => {
452         let entry = gPermissions.get(permission.type);
453         if (!entry || entry.disabled) {
454           return false;
455         }
456         let type = entry.id;
458         /* Hide persistent storage permission when extension principal
459          * have WebExtensions-unlimitedStorage permission. */
460         if (
461           type == "persistent-storage" &&
462           SitePermissions.getForPrincipal(
463             principal,
464             "WebExtensions-unlimitedStorage"
465           ).state == SitePermissions.ALLOW
466         ) {
467           return false;
468         }
470         return true;
471       });
473     return permissions.map(permission => {
474       let scope = this.SCOPE_PERSISTENT;
475       if (permission.expireType == Services.perms.EXPIRE_SESSION) {
476         scope = this.SCOPE_SESSION;
477       } else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
478         scope = this.SCOPE_POLICY;
479       }
481       return {
482         id: permission.type,
483         scope,
484         state: permission.capability,
485       };
486     });
487   },
489   /**
490    * Returns all custom permissions for a given browser.
491    *
492    * To receive a more detailed, albeit less performant listing see
493    * SitePermissions.getAllPermissionDetailsForBrowser().
494    *
495    * @param {Browser} browser
496    *        The browser to fetch permission for.
497    *
498    * @return {Array} a list of objects with the keys:
499    *         - id: the permissionId of the permission
500    *         - state: a constant representing the current permission state
501    *           (e.g. SitePermissions.ALLOW)
502    *         - scope: a constant representing how long the permission will
503    *           be kept.
504    */
505   getAllForBrowser(browser) {
506     let permissions = {};
508     for (let permission of TemporaryPermissions.getAll(browser)) {
509       permission.scope = this.SCOPE_TEMPORARY;
510       permissions[permission.id] = permission;
511     }
513     for (let permission of GloballyBlockedPermissions.getAll(browser)) {
514       permissions[permission.id] = permission;
515     }
517     for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) {
518       permissions[permission.id] = permission;
519     }
521     return Object.values(permissions);
522   },
524   /**
525    * Returns a list of objects with detailed information on all permissions
526    * that are currently set for the given browser.
527    *
528    * @param {Browser} browser
529    *        The browser to fetch permission for.
530    *
531    * @return {Array<Object>} a list of objects with the keys:
532    *           - id: the permissionID of the permission
533    *           - state: a constant representing the current permission state
534    *             (e.g. SitePermissions.ALLOW)
535    *           - scope: a constant representing how long the permission will
536    *             be kept.
537    *           - label: the localized label, or null if none is available.
538    */
539   getAllPermissionDetailsForBrowser(browser) {
540     return this.getAllForBrowser(browser).map(({ id, scope, state }) => ({
541       id,
542       scope,
543       state,
544       label: this.getPermissionLabel(id),
545     }));
546   },
548   /**
549    * Checks whether a UI for managing permissions should be exposed for a given
550    * principal.
551    *
552    * @param {nsIPrincipal} principal
553    *        The principal to check.
554    *
555    * @return {boolean} if the principal is supported.
556    */
557   isSupportedPrincipal(principal) {
558     if (!principal) {
559       return false;
560     }
561     if (!(principal instanceof Ci.nsIPrincipal)) {
562       throw new Error(
563         "Argument passed as principal is not an instance of Ci.nsIPrincipal"
564       );
565     }
566     return this.isSupportedScheme(principal.scheme);
567   },
569   /**
570    * Checks whether we support managing permissions for a specific scheme.
571    * @param {string} scheme - Scheme to test.
572    * @returns {boolean} Whether the scheme is supported.
573    */
574   isSupportedScheme(scheme) {
575     return ["http", "https", "moz-extension", "file"].includes(scheme);
576   },
578   /**
579    * Gets an array of all permission IDs.
580    *
581    * @return {Array<String>} an array of all permission IDs.
582    */
583   listPermissions() {
584     if (this._permissionsArray === null) {
585       this._permissionsArray = gPermissions.getEnabledPermissions();
586     }
587     return this._permissionsArray;
588   },
590   /**
591    * Test whether a permission is managed by SitePermissions.
592    * @param {string} type - Permission type.
593    * @returns {boolean}
594    */
595   isSitePermission(type) {
596     return gPermissions.has(type);
597   },
599   /**
600    * Called when a preference changes its value.
601    *
602    * @param {string} data
603    *        The last argument passed to the preference change observer
604    * @param {string} previous
605    *        The previous value of the preference
606    * @param {string} latest
607    *        The latest value of the preference
608    */
609   invalidatePermissionList(data, previous, latest) {
610     // Ensure that listPermissions() will reconstruct its return value the next
611     // time it's called.
612     this._permissionsArray = null;
613   },
615   /**
616    * Returns an array of permission states to be exposed to the user for a
617    * permission with the given ID.
618    *
619    * @param {string} permissionID
620    *        The ID to get permission states for.
621    *
622    * @return {Array<SitePermissions state>} an array of all permission states.
623    */
624   getAvailableStates(permissionID) {
625     if (
626       gPermissions.has(permissionID) &&
627       gPermissions.get(permissionID).states
628     ) {
629       return gPermissions.get(permissionID).states;
630     }
632     /* Since the permissions we are dealing with have adopted the convention
633      * of treating UNKNOWN == PROMPT, we only include one of either UNKNOWN
634      * or PROMPT in this list, to avoid duplicating states. */
635     if (this.getDefault(permissionID) == this.UNKNOWN) {
636       return [
637         SitePermissions.UNKNOWN,
638         SitePermissions.ALLOW,
639         SitePermissions.BLOCK,
640       ];
641     }
643     return [
644       SitePermissions.PROMPT,
645       SitePermissions.ALLOW,
646       SitePermissions.BLOCK,
647     ];
648   },
650   /**
651    * Returns the default state of a particular permission.
652    *
653    * @param {string} permissionID
654    *        The ID to get the default for.
655    *
656    * @return {SitePermissions.state} the default state.
657    */
658   getDefault(permissionID) {
659     // If the permission has custom logic for getting its default value,
660     // try that first.
661     if (
662       gPermissions.has(permissionID) &&
663       gPermissions.get(permissionID).getDefault
664     ) {
665       return gPermissions.get(permissionID).getDefault();
666     }
668     // Otherwise try to get the default preference for that permission.
669     return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
670   },
672   /**
673    * Set the default state of a particular permission.
674    *
675    * @param {string} permissionID
676    *        The ID to set the default for.
677    *
678    * @param {string} state
679    *        The state to set.
680    */
681   setDefault(permissionID, state) {
682     if (
683       gPermissions.has(permissionID) &&
684       gPermissions.get(permissionID).setDefault
685     ) {
686       return gPermissions.get(permissionID).setDefault(state);
687     }
688     let key = "permissions.default." + permissionID;
689     return Services.prefs.setIntPref(key, state);
690   },
692   /**
693    * Returns the state and scope of a particular permission for a given principal.
694    *
695    * This method will NOT dispatch a "PermissionStateChange" event on the specified
696    * browser if a temporary permission was removed because it has expired.
697    *
698    * @param {nsIPrincipal} principal
699    *        The principal to check.
700    * @param {String} permissionID
701    *        The id of the permission.
702    * @param {Browser} browser (optional)
703    *        The browser object to check for temporary permissions.
704    *
705    * @return {Object} an object with the keys:
706    *           - state: The current state of the permission
707    *             (e.g. SitePermissions.ALLOW)
708    *           - scope: The scope of the permission
709    *             (e.g. SitePermissions.SCOPE_PERSISTENT)
710    */
711   getForPrincipal(principal, permissionID, browser) {
712     if (!principal && !browser) {
713       throw new Error(
714         "Atleast one of the arguments, either principal or browser should not be null."
715       );
716     }
717     let defaultState = this.getDefault(permissionID);
718     let result = { state: defaultState, scope: this.SCOPE_PERSISTENT };
719     if (this.isSupportedPrincipal(principal)) {
720       let permission = null;
721       if (
722         gPermissions.has(permissionID) &&
723         gPermissions.get(permissionID).exactHostMatch
724       ) {
725         permission = Services.perms.getPermissionObject(
726           principal,
727           permissionID,
728           true
729         );
730       } else {
731         permission = Services.perms.getPermissionObject(
732           principal,
733           permissionID,
734           false
735         );
736       }
738       if (permission) {
739         result.state = permission.capability;
740         if (permission.expireType == Services.perms.EXPIRE_SESSION) {
741           result.scope = this.SCOPE_SESSION;
742         } else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
743           result.scope = this.SCOPE_POLICY;
744         }
745       }
746     }
748     if (result.state == defaultState) {
749       // If there's no persistent permission saved, check if we have something
750       // set temporarily.
751       let value = TemporaryPermissions.get(browser, permissionID);
753       if (value) {
754         result.state = value.state;
755         result.scope = this.SCOPE_TEMPORARY;
756       }
757     }
759     return result;
760   },
762   /**
763    * Sets the state of a particular permission for a given principal or browser.
764    * This method will dispatch a "PermissionStateChange" event on the specified
765    * browser if a temporary permission was set
766    *
767    * @param {nsIPrincipal} principal
768    *        The principal to set the permission for.
769    *        Note that this will be ignored if the scope is set to SCOPE_TEMPORARY
770    * @param {String} permissionID
771    *        The id of the permission.
772    * @param {SitePermissions state} state
773    *        The state of the permission.
774    * @param {SitePermissions scope} scope (optional)
775    *        The scope of the permission. Defaults to SCOPE_PERSISTENT.
776    * @param {Browser} browser (optional)
777    *        The browser object to set temporary permissions on.
778    *        This needs to be provided if the scope is SCOPE_TEMPORARY!
779    * @param {number} expireTimeMS (optional) If setting a temporary permission,
780    *        how many milliseconds it should be valid for.
781    * @param {nsIURI} browserURI (optional) Pass a custom URI for the
782    *        temporary permission scope. This defaults to the current URI of the
783    *        browser.
784    */
785   setForPrincipal(
786     principal,
787     permissionID,
788     state,
789     scope = this.SCOPE_PERSISTENT,
790     browser = null,
791     expireTimeMS = SitePermissions.temporaryPermissionExpireTime,
792     browserURI = browser?.currentURI
793   ) {
794     if (!principal && !browser) {
795       throw new Error(
796         "Atleast one of the arguments, either principal or browser should not be null."
797       );
798     }
799     if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
800       GloballyBlockedPermissions.set(browser, permissionID);
801       browser.dispatchEvent(
802         new browser.ownerGlobal.CustomEvent("PermissionStateChange")
803       );
804       return;
805     }
807     if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
808       // Because they are controlled by two prefs with many states that do not
809       // correspond to the classical ALLOW/DENY/PROMPT model, we want to always
810       // allow the user to add exceptions to their cookie rules without removing them.
811       if (permissionID != "cookie") {
812         this.removeFromPrincipal(principal, permissionID, browser);
813         return;
814       }
815     }
817     if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
818       throw new Error(
819         "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission"
820       );
821     }
823     // Save temporary permissions.
824     if (scope == this.SCOPE_TEMPORARY) {
825       if (!browser) {
826         throw new Error(
827           "TEMPORARY scoped permissions require a browser object"
828         );
829       }
830       if (!Number.isInteger(expireTimeMS) || expireTimeMS <= 0) {
831         throw new Error("expireTime must be a positive integer");
832       }
834       TemporaryPermissions.set(
835         browser,
836         permissionID,
837         state,
838         expireTimeMS,
839         browserURI,
840         // On permission expiry
841         origBrowser => {
842           if (!origBrowser.ownerGlobal) {
843             return;
844           }
845           origBrowser.dispatchEvent(
846             new origBrowser.ownerGlobal.CustomEvent("PermissionStateChange")
847           );
848         }
849       );
851       browser.dispatchEvent(
852         new browser.ownerGlobal.CustomEvent("PermissionStateChange")
853       );
854     } else if (this.isSupportedPrincipal(principal)) {
855       let perms_scope = Services.perms.EXPIRE_NEVER;
856       if (scope == this.SCOPE_SESSION) {
857         perms_scope = Services.perms.EXPIRE_SESSION;
858       } else if (scope == this.SCOPE_POLICY) {
859         perms_scope = Services.perms.EXPIRE_POLICY;
860       }
862       Services.perms.addFromPrincipal(
863         principal,
864         permissionID,
865         state,
866         perms_scope
867       );
868     }
869   },
871   /**
872    * Removes the saved state of a particular permission for a given principal and/or browser.
873    * This method will dispatch a "PermissionStateChange" event on the specified
874    * browser if a temporary permission was removed.
875    *
876    * @param {nsIPrincipal} principal
877    *        The principal to remove the permission for.
878    * @param {String} permissionID
879    *        The id of the permission.
880    * @param {Browser} browser (optional)
881    *        The browser object to remove temporary permissions on.
882    */
883   removeFromPrincipal(principal, permissionID, browser) {
884     if (!principal && !browser) {
885       throw new Error(
886         "Atleast one of the arguments, either principal or browser should not be null."
887       );
888     }
889     if (this.isSupportedPrincipal(principal)) {
890       Services.perms.removeFromPrincipal(principal, permissionID);
891     }
893     // TemporaryPermissions.get() deletes expired permissions automatically,
894     if (TemporaryPermissions.get(browser, permissionID)) {
895       // If it exists but has not expired, remove it explicitly.
896       TemporaryPermissions.remove(browser, permissionID);
897       // Send a PermissionStateChange event only if the permission hasn't expired.
898       browser.dispatchEvent(
899         new browser.ownerGlobal.CustomEvent("PermissionStateChange")
900       );
901     }
902   },
904   /**
905    * Clears all block permissions that were temporarily saved.
906    *
907    * @param {Browser} browser
908    *        The browser object to clear.
909    */
910   clearTemporaryBlockPermissions(browser) {
911     TemporaryPermissions.clear(browser, SitePermissions.BLOCK);
912   },
914   /**
915    * Copy all permissions that were temporarily saved on one
916    * browser object to a new browser.
917    *
918    * @param {Browser} browser
919    *        The browser object to copy from.
920    * @param {Browser} newBrowser
921    *        The browser object to copy to.
922    */
923   copyTemporaryPermissions(browser, newBrowser) {
924     TemporaryPermissions.copy(browser, newBrowser);
925     GloballyBlockedPermissions.copy(browser, newBrowser);
926   },
928   /**
929    * Returns the localized label for the permission with the given ID, to be
930    * used in a UI for managing permissions.
931    * If a permission is double keyed (has an additional key in the ID), the
932    * second key is split off and supplied to the string formatter as a variable.
933    *
934    * @param {string} permissionID
935    *        The permission to get the label for. May include second key.
936    *
937    * @return {String} the localized label or null if none is available.
938    */
939   getPermissionLabel(permissionID) {
940     let [id, key] = permissionID.split(this.PERM_KEY_DELIMITER);
941     if (!gPermissions.has(id)) {
942       // Permission can't be found.
943       return null;
944     }
945     if (
946       "labelID" in gPermissions.get(id) &&
947       gPermissions.get(id).labelID === null
948     ) {
949       // Permission doesn't support having a label.
950       return null;
951     }
952     if (id == "3rdPartyStorage") {
953       // The key is the 3rd party origin, which we use for the label.
954       return key;
955     }
956     let labelID = gPermissions.get(id).labelID || id;
957     return gStringBundle.formatStringFromName(`permission.${labelID}.label`, [
958       key,
959     ]);
960   },
962   /**
963    * Returns the localized label for the given permission state, to be used in
964    * a UI for managing permissions.
965    *
966    * @param {string} permissionID
967    *        The permission to get the label for.
968    *
969    * @param {SitePermissions state} state
970    *        The state to get the label for.
971    *
972    * @return {String|null} the localized label or null if an
973    *         unknown state was passed.
974    */
975   getMultichoiceStateLabel(permissionID, state) {
976     // If the permission has custom logic for getting its default value,
977     // try that first.
978     if (
979       gPermissions.has(permissionID) &&
980       gPermissions.get(permissionID).getMultichoiceStateLabel
981     ) {
982       return gPermissions.get(permissionID).getMultichoiceStateLabel(state);
983     }
985     switch (state) {
986       case this.UNKNOWN:
987       case this.PROMPT:
988         return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk");
989       case this.ALLOW:
990         return gStringBundle.GetStringFromName("state.multichoice.allow");
991       case this.ALLOW_COOKIES_FOR_SESSION:
992         return gStringBundle.GetStringFromName(
993           "state.multichoice.allowForSession"
994         );
995       case this.BLOCK:
996         return gStringBundle.GetStringFromName("state.multichoice.block");
997       default:
998         return null;
999     }
1000   },
1002   /**
1003    * Returns the localized label for a permission's current state.
1004    *
1005    * @param {SitePermissions state} state
1006    *        The state to get the label for.
1007    * @param {string} id
1008    *        The permission to get the state label for.
1009    * @param {SitePermissions scope} scope (optional)
1010    *        The scope to get the label for.
1011    *
1012    * @return {String|null} the localized label or null if an
1013    *         unknown state was passed.
1014    */
1015   getCurrentStateLabel(state, id, scope = null) {
1016     switch (state) {
1017       case this.PROMPT:
1018         return gStringBundle.GetStringFromName("state.current.prompt");
1019       case this.ALLOW:
1020         if (
1021           scope &&
1022           scope != this.SCOPE_PERSISTENT &&
1023           scope != this.SCOPE_POLICY
1024         ) {
1025           return gStringBundle.GetStringFromName(
1026             "state.current.allowedTemporarily"
1027           );
1028         }
1029         return gStringBundle.GetStringFromName("state.current.allowed");
1030       case this.ALLOW_COOKIES_FOR_SESSION:
1031         return gStringBundle.GetStringFromName(
1032           "state.current.allowedForSession"
1033         );
1034       case this.BLOCK:
1035         if (
1036           scope &&
1037           scope != this.SCOPE_PERSISTENT &&
1038           scope != this.SCOPE_POLICY &&
1039           scope != this.SCOPE_GLOBAL
1040         ) {
1041           return gStringBundle.GetStringFromName(
1042             "state.current.blockedTemporarily"
1043           );
1044         }
1045         return gStringBundle.GetStringFromName("state.current.blocked");
1046       default:
1047         return null;
1048     }
1049   },
1052 let gPermissions = {
1053   _getId(type) {
1054     // Split off second key (if it exists).
1055     let [id] = type.split(SitePermissions.PERM_KEY_DELIMITER);
1056     return id;
1057   },
1059   has(type) {
1060     return this._getId(type) in this._permissions;
1061   },
1063   get(type) {
1064     let id = this._getId(type);
1065     let perm = this._permissions[id];
1066     if (perm) {
1067       perm.id = id;
1068     }
1069     return perm;
1070   },
1072   getEnabledPermissions() {
1073     return Object.keys(this._permissions).filter(
1074       id => !this._permissions[id].disabled
1075     );
1076   },
1078   /* Holds permission ID => options pairs.
1079    *
1080    * Supported options:
1081    *
1082    *  - exactHostMatch
1083    *    Allows sub domains to have their own permissions.
1084    *    Defaults to false.
1085    *
1086    *  - getDefault
1087    *    Called to get the permission's default state.
1088    *    Defaults to UNKNOWN, indicating that the user will be asked each time
1089    *    a page asks for that permissions.
1090    *
1091    *  - labelID
1092    *    Use the given ID instead of the permission name for looking up strings.
1093    *    e.g. "desktop-notification2" to use permission.desktop-notification2.label
1094    *
1095    *  - states
1096    *    Array of permission states to be exposed to the user.
1097    *    Defaults to ALLOW, BLOCK and the default state (see getDefault).
1098    *
1099    *  - getMultichoiceStateLabel
1100    *    Optional method to overwrite SitePermissions#getMultichoiceStateLabel with custom label logic.
1101    */
1102   _permissions: {
1103     "autoplay-media": {
1104       exactHostMatch: true,
1105       getDefault() {
1106         let pref = Services.prefs.getIntPref(
1107           "media.autoplay.default",
1108           Ci.nsIAutoplay.BLOCKED
1109         );
1110         if (pref == Ci.nsIAutoplay.ALLOWED) {
1111           return SitePermissions.ALLOW;
1112         }
1113         if (pref == Ci.nsIAutoplay.BLOCKED_ALL) {
1114           return SitePermissions.AUTOPLAY_BLOCKED_ALL;
1115         }
1116         return SitePermissions.BLOCK;
1117       },
1118       setDefault(value) {
1119         let prefValue = Ci.nsIAutoplay.BLOCKED;
1120         if (value == SitePermissions.ALLOW) {
1121           prefValue = Ci.nsIAutoplay.ALLOWED;
1122         } else if (value == SitePermissions.AUTOPLAY_BLOCKED_ALL) {
1123           prefValue = Ci.nsIAutoplay.BLOCKED_ALL;
1124         }
1125         Services.prefs.setIntPref("media.autoplay.default", prefValue);
1126       },
1127       labelID: "autoplay",
1128       states: [
1129         SitePermissions.ALLOW,
1130         SitePermissions.BLOCK,
1131         SitePermissions.AUTOPLAY_BLOCKED_ALL,
1132       ],
1133       getMultichoiceStateLabel(state) {
1134         switch (state) {
1135           case SitePermissions.AUTOPLAY_BLOCKED_ALL:
1136             return gStringBundle.GetStringFromName(
1137               "state.multichoice.autoplayblockall"
1138             );
1139           case SitePermissions.BLOCK:
1140             return gStringBundle.GetStringFromName(
1141               "state.multichoice.autoplayblock"
1142             );
1143           case SitePermissions.ALLOW:
1144             return gStringBundle.GetStringFromName(
1145               "state.multichoice.autoplayallow"
1146             );
1147         }
1148         throw new Error(`Unknown state: ${state}`);
1149       },
1150     },
1152     cookie: {
1153       states: [
1154         SitePermissions.ALLOW,
1155         SitePermissions.ALLOW_COOKIES_FOR_SESSION,
1156         SitePermissions.BLOCK,
1157       ],
1158       getDefault() {
1159         if (
1160           Services.cookies.getCookieBehavior(false) ==
1161           Ci.nsICookieService.BEHAVIOR_REJECT
1162         ) {
1163           return SitePermissions.BLOCK;
1164         }
1166         if (
1167           Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
1168           Ci.nsICookieService.ACCEPT_SESSION
1169         ) {
1170           return SitePermissions.ALLOW_COOKIES_FOR_SESSION;
1171         }
1173         return SitePermissions.ALLOW;
1174       },
1175     },
1177     "desktop-notification": {
1178       exactHostMatch: true,
1179       labelID: "desktop-notification3",
1180     },
1182     camera: {
1183       exactHostMatch: true,
1184     },
1186     microphone: {
1187       exactHostMatch: true,
1188     },
1190     screen: {
1191       exactHostMatch: true,
1192       states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK],
1193     },
1195     popup: {
1196       getDefault() {
1197         return Services.prefs.getBoolPref("dom.disable_open_during_load")
1198           ? SitePermissions.BLOCK
1199           : SitePermissions.ALLOW;
1200       },
1201       states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1202     },
1204     install: {
1205       getDefault() {
1206         return Services.prefs.getBoolPref("xpinstall.whitelist.required")
1207           ? SitePermissions.UNKNOWN
1208           : SitePermissions.ALLOW;
1209       },
1210     },
1212     geo: {
1213       exactHostMatch: true,
1214     },
1216     "open-protocol-handler": {
1217       labelID: "open-protocol-handler",
1218       exactHostMatch: true,
1219       states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1220       get disabled() {
1221         return !SitePermissions.openProtoPermissionEnabled;
1222       },
1223     },
1225     xr: {
1226       exactHostMatch: true,
1227     },
1229     "focus-tab-by-prompt": {
1230       exactHostMatch: true,
1231       states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW],
1232     },
1233     "persistent-storage": {
1234       exactHostMatch: true,
1235     },
1237     shortcuts: {
1238       states: [SitePermissions.ALLOW, SitePermissions.BLOCK],
1239     },
1241     canvas: {
1242       get disabled() {
1243         return !SitePermissions.resistFingerprinting;
1244       },
1245     },
1247     midi: {
1248       exactHostMatch: true,
1249       get disabled() {
1250         return !SitePermissions.midiPermissionEnabled;
1251       },
1252     },
1254     "midi-sysex": {
1255       exactHostMatch: true,
1256       get disabled() {
1257         return !SitePermissions.midiPermissionEnabled;
1258       },
1259     },
1261     "storage-access": {
1262       labelID: null,
1263       getDefault() {
1264         return SitePermissions.UNKNOWN;
1265       },
1266     },
1268     "3rdPartyStorage": {
1269       get disabled() {
1270         return !SitePermissions.statePartitioningPermissionsEnabled;
1271       },
1272     },
1273   },
1276 SitePermissions.midiPermissionEnabled = Services.prefs.getBoolPref(
1277   "dom.webmidi.enabled"
1280 XPCOMUtils.defineLazyPreferenceGetter(
1281   SitePermissions,
1282   "temporaryPermissionExpireTime",
1283   "privacy.temporary_permission_expire_time_ms",
1284   3600 * 1000
1286 XPCOMUtils.defineLazyPreferenceGetter(
1287   SitePermissions,
1288   "resistFingerprinting",
1289   "privacy.resistFingerprinting",
1290   false,
1291   SitePermissions.invalidatePermissionList.bind(SitePermissions)
1293 XPCOMUtils.defineLazyPreferenceGetter(
1294   SitePermissions,
1295   "openProtoPermissionEnabled",
1296   "security.external_protocol_requires_permission",
1297   true,
1298   SitePermissions.invalidatePermissionList.bind(SitePermissions)
1300 XPCOMUtils.defineLazyPreferenceGetter(
1301   SitePermissions,
1302   "statePartitioningPermissionsEnabled",
1303   "browser.contentblocking.state-partitioning.mvp.ui.enabled",
1304   false,
1305   SitePermissions.invalidatePermissionList.bind(SitePermissions)