Bug 1862332 [wpt PR 42877] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / browser / modules / PermissionUI.sys.mjs
blobe94beb79ac3dafb02e6dc9f9c877a7bb300a56f2
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 /**
6  * PermissionUI is responsible for exposing both a prototype
7  * PermissionPrompt that can be used by arbitrary browser
8  * components and add-ons, but also hosts the implementations of
9  * built-in permission prompts.
10  *
11  * If you're developing a feature that requires web content to ask
12  * for special permissions from the user, this module is for you.
13  *
14  * Suppose a system add-on wants to add a new prompt for a new request
15  * for getting more low-level access to the user's sound card, and the
16  * permission request is coming up from content by way of the
17  * nsContentPermissionHelper. The system add-on could then do the following:
18  *
19  * const { Integration } = ChromeUtils.importESModule(
20  *   "resource://gre/modules/Integration.sys.mjs"
21  * );
22  * const { PermissionUI } = ChromeUtils.importESModule(
23  *   "resource:///modules/PermissionUI.sys.mjs"
24  * );
25  *
26  * const SoundCardIntegration = base => {
27  *   let soundCardObj = {
28  *     createPermissionPrompt(type, request) {
29  *       if (type != "sound-api") {
30  *         return super.createPermissionPrompt(...arguments);
31  *       }
32  *
33  *       let permissionPrompt = {
34  *         get permissionKey() {
35  *           return "sound-permission";
36  *         }
37  *         // etc - see the documentation for PermissionPrompt for
38  *         // a better idea of what things one can and should override.
39  *       };
40  *       Object.setPrototypeOf(
41  *         permissionPrompt,
42  *         PermissionUI.PermissionPromptForRequest
43  *       );
44  *       return permissionPrompt;
45  *     },
46  *   };
47  *   Object.setPrototypeOf(soundCardObj, base);
48  *   return soundCardObj;
49  * };
50  *
51  * // Add-on startup:
52  * Integration.contentPermission.register(SoundCardIntegration);
53  * // ...
54  * // Add-on shutdown:
55  * Integration.contentPermission.unregister(SoundCardIntegration);
56  *
57  * Note that PermissionPromptForRequest must be used as the
58  * prototype, since the prompt is wrapping an nsIContentPermissionRequest,
59  * and going through nsIContentPermissionPrompt.
60  *
61  * It is, however, possible to take advantage of PermissionPrompt without
62  * having to go through nsIContentPermissionPrompt or with a
63  * nsIContentPermissionRequest. The PermissionPrompt can be
64  * imported, subclassed, and have prompt() called directly, without
65  * the caller having called into createPermissionPrompt.
66  */
67 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
69 const lazy = {};
71 ChromeUtils.defineESModuleGetters(lazy, {
72   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
73   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
74   SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
75 });
77 XPCOMUtils.defineLazyServiceGetter(
78   lazy,
79   "IDNService",
80   "@mozilla.org/network/idn-service;1",
81   "nsIIDNService"
83 XPCOMUtils.defineLazyServiceGetter(
84   lazy,
85   "ContentPrefService2",
86   "@mozilla.org/content-pref/service;1",
87   "nsIContentPrefService2"
89 ChromeUtils.defineLazyGetter(lazy, "gBrowserBundle", function () {
90   return Services.strings.createBundle(
91     "chrome://browser/locale/browser.properties"
92   );
93 });
95 import { SITEPERMS_ADDON_PROVIDER_PREF } from "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs";
97 XPCOMUtils.defineLazyPreferenceGetter(
98   lazy,
99   "sitePermsAddonsProviderEnabled",
100   SITEPERMS_ADDON_PROVIDER_PREF,
101   false
105  * PermissionPrompt should be subclassed by callers that
106  * want to display prompts to the user. See each method and property
107  * below for guidance on what to override.
109  * Note that if you're creating a prompt for an
110  * nsIContentPermissionRequest, you'll want to subclass
111  * PermissionPromptForRequest instead.
112  */
113 class PermissionPrompt {
114   /**
115    * Returns the associated <xul:browser> for the request. This should
116    * work for the e10s and non-e10s case.
117    *
118    * Subclasses must override this.
119    *
120    * @return {<xul:browser>}
121    */
122   get browser() {
123     throw new Error("Not implemented.");
124   }
126   /**
127    * Returns the nsIPrincipal associated with the request.
128    *
129    * Subclasses must override this.
130    *
131    * @return {nsIPrincipal}
132    */
133   get principal() {
134     throw new Error("Not implemented.");
135   }
137   /**
138    * Indicates the type of the permission request from content. This type might
139    * be different from the permission key used in the permissions database.
140    */
141   get type() {
142     return undefined;
143   }
145   /**
146    * If the nsIPermissionManager is being queried and written
147    * to for this permission request, set this to the key to be
148    * used. If this is undefined, no integration with temporary
149    * permissions infrastructure will be provided.
150    *
151    * Note that if a permission is set, in any follow-up
152    * prompting within the expiry window of that permission,
153    * the prompt will be skipped and the allow or deny choice
154    * will be selected automatically.
155    */
156   get permissionKey() {
157     return undefined;
158   }
160   /**
161    * If true, user permissions will be read from and written to.
162    * When this is false, we still provide integration with
163    * infrastructure such as temporary permissions. permissionKey should
164    * still return a valid name in those cases for that integration to work.
165    */
166   get usePermissionManager() {
167     return true;
168   }
170   /**
171    * Indicates what URI should be used as the scope when using temporary
172    * permissions. If undefined, it defaults to the browser.currentURI.
173    */
174   get temporaryPermissionURI() {
175     return undefined;
176   }
178   /**
179    * These are the options that will be passed to the PopupNotification when it
180    * is shown. See the documentation of `PopupNotifications_show` in
181    * PopupNotifications.sys.mjs for details.
182    *
183    * Note that prompt() will automatically set displayURI to
184    * be the URI of the requesting pricipal, unless the displayURI is exactly
185    * set to false.
186    */
187   get popupOptions() {
188     return {};
189   }
191   /**
192    * If true, automatically denied permission requests will
193    * spawn a "post-prompt" that allows the user to correct the
194    * automatic denial by giving permanent permission access to
195    * the site.
196    *
197    * Note that if this function returns true, the permissionKey
198    * and postPromptActions attributes must be implemented.
199    */
200   get postPromptEnabled() {
201     return false;
202   }
204   /**
205    * If true, the prompt will be cancelled automatically unless
206    * request.hasValidTransientUserGestureActivation is true.
207    */
208   get requiresUserInput() {
209     return false;
210   }
212   /**
213    * PopupNotification requires a unique ID to open the notification.
214    * You must return a unique ID string here, for which PopupNotification
215    * will then create a <xul:popupnotification> node with the ID
216    * "<notificationID>-notification".
217    *
218    * If there's a custom <xul:popupnotification> you're hoping to show,
219    * then you need to make sure its ID has the "-notification" suffix,
220    * and then return the prefix here.
221    *
222    * See PopupNotifications.sys.mjs for more details.
223    *
224    * @return {string}
225    *         The unique ID that will be used to as the
226    *         "<unique ID>-notification" ID for the <xul:popupnotification>
227    *         to use or create.
228    */
229   get notificationID() {
230     throw new Error("Not implemented.");
231   }
233   /**
234    * The ID of the element to anchor the PopupNotification to.
235    *
236    * @return {string}
237    */
238   get anchorID() {
239     return "default-notification-icon";
240   }
242   /**
243    * The message to show to the user in the PopupNotification, see
244    * `PopupNotifications_show` in PopupNotifications.sys.mjs.
245    *
246    * Subclasses must override this.
247    *
248    * @return {string}
249    */
250   get message() {
251     throw new Error("Not implemented.");
252   }
254   /**
255    * Provides the preferred name to use in the permission popups,
256    * based on the principal URI (the URI.hostPort for any URI scheme
257    * besides the moz-extension one which should default to the
258    * extension name).
259    */
260   getPrincipalName(principal = this.principal) {
261     if (principal.addonPolicy) {
262       return principal.addonPolicy.name;
263     }
265     return principal.hostPort;
266   }
268   /**
269    * This will be called if the request is to be cancelled.
270    *
271    * Subclasses only need to override this if they provide a
272    * permissionKey.
273    */
274   cancel() {
275     throw new Error("Not implemented.");
276   }
278   /**
279    * This will be called if the request is to be allowed.
280    *
281    * Subclasses only need to override this if they provide a
282    * permissionKey.
283    */
284   allow() {
285     throw new Error("Not implemented.");
286   }
288   /**
289    * The actions that will be displayed in the PopupNotification
290    * via a dropdown menu. The first item in this array will be
291    * the default selection. Each action is an Object with the
292    * following properties:
293    *
294    *  label (string):
295    *    The label that will be displayed for this choice.
296    *  accessKey (string):
297    *    The access key character that will be used for this choice.
298    *  action (SitePermissions state)
299    *    The action that will be associated with this choice.
300    *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
301    *  scope (SitePermissions scope)
302    *    The scope of the associated action (e.g. SitePermissions.SCOPE_PERSISTENT)
303    *
304    *  callback (function, optional)
305    *    A callback function that will fire if the user makes this choice, with
306    *    a single parameter, state. State is an Object that contains the property
307    *    checkboxChecked, which identifies whether the checkbox to remember this
308    *    decision was checked.
309    */
310   get promptActions() {
311     return [];
312   }
314   /**
315    * The actions that will be displayed in the PopupNotification
316    * for post-prompt notifications via a dropdown menu.
317    * The first item in this array will be the default selection.
318    * Each action is an Object with the following properties:
319    *
320    *  label (string):
321    *    The label that will be displayed for this choice.
322    *  accessKey (string):
323    *    The access key character that will be used for this choice.
324    *  action (SitePermissions state)
325    *    The action that will be associated with this choice.
326    *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
327    *    Note that the scope of this action will always be persistent.
328    *
329    *  callback (function, optional)
330    *    A callback function that will fire if the user makes this choice.
331    */
332   get postPromptActions() {
333     return null;
334   }
336   /**
337    * If the prompt will be shown to the user, this callback will
338    * be called just before. Subclasses may want to override this
339    * in order to, for example, bump a counter Telemetry probe for
340    * how often a particular permission request is seen.
341    *
342    * If this returns false, it cancels the process of showing the prompt.  In
343    * that case, it is the responsibility of the onBeforeShow() implementation
344    * to ensure that allow() or cancel() are called on the object appropriately.
345    */
346   onBeforeShow() {
347     return true;
348   }
350   /**
351    * If the prompt was shown to the user, this callback will be called just
352    * after it's been shown.
353    */
354   onShown() {}
356   /**
357    * If the prompt was shown to the user, this callback will be called just
358    * after it's been hidden.
359    */
360   onAfterShow() {}
362   /**
363    * Will determine if a prompt should be shown to the user, and if so,
364    * will show it.
365    *
366    * If a permissionKey is defined prompt() might automatically
367    * allow or cancel itself based on the user's current
368    * permission settings without displaying the prompt.
369    *
370    * If the permission is not already set and the <xul:browser> that the request
371    * is associated with does not belong to a browser window with the
372    * PopupNotifications global set, the prompt request is ignored.
373    */
374   prompt() {
375     // We ignore requests from non-nsIStandardURLs
376     let requestingURI = this.principal.URI;
377     if (!(requestingURI instanceof Ci.nsIStandardURL)) {
378       return;
379     }
381     if (this.usePermissionManager && this.permissionKey) {
382       // If we're reading and setting permissions, then we need
383       // to check to see if we already have a permission setting
384       // for this particular principal.
385       let { state } = lazy.SitePermissions.getForPrincipal(
386         this.principal,
387         this.permissionKey,
388         this.browser,
389         this.temporaryPermissionURI
390       );
392       if (state == lazy.SitePermissions.BLOCK) {
393         this.cancel();
394         return;
395       }
397       if (
398         state == lazy.SitePermissions.ALLOW &&
399         !this.request.isRequestDelegatedToUnsafeThirdParty
400       ) {
401         this.allow();
402         return;
403       }
404     } else if (this.permissionKey) {
405       // If we're reading a permission which already has a temporary value,
406       // see if we can use the temporary value.
407       let { state } = lazy.SitePermissions.getForPrincipal(
408         null,
409         this.permissionKey,
410         this.browser,
411         this.temporaryPermissionURI
412       );
414       if (state == lazy.SitePermissions.BLOCK) {
415         this.cancel();
416         return;
417       }
418     }
420     if (
421       this.requiresUserInput &&
422       !this.request.hasValidTransientUserGestureActivation
423     ) {
424       if (this.postPromptEnabled) {
425         this.postPrompt();
426       }
427       this.cancel();
428       return;
429     }
431     let chromeWin = this.browser.ownerGlobal;
432     if (!chromeWin.PopupNotifications) {
433       this.cancel();
434       return;
435     }
437     // Transform the PermissionPrompt actions into PopupNotification actions.
438     let popupNotificationActions = [];
439     for (let promptAction of this.promptActions) {
440       let action = {
441         label: promptAction.label,
442         accessKey: promptAction.accessKey,
443         callback: state => {
444           if (promptAction.callback) {
445             promptAction.callback();
446           }
448           if (this.usePermissionManager && this.permissionKey) {
449             if (
450               (state && state.checkboxChecked && state.source != "esc-press") ||
451               promptAction.scope == lazy.SitePermissions.SCOPE_PERSISTENT
452             ) {
453               // Permanently store permission.
454               let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
455               // Only remember permission for session if in PB mode.
456               if (lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
457                 scope = lazy.SitePermissions.SCOPE_SESSION;
458               }
459               lazy.SitePermissions.setForPrincipal(
460                 this.principal,
461                 this.permissionKey,
462                 promptAction.action,
463                 scope
464               );
465             } else if (promptAction.action == lazy.SitePermissions.BLOCK) {
466               // Temporarily store BLOCK permissions only
467               // SitePermissions does not consider subframes when storing temporary
468               // permissions on a tab, thus storing ALLOW could be exploited.
469               lazy.SitePermissions.setForPrincipal(
470                 this.principal,
471                 this.permissionKey,
472                 promptAction.action,
473                 lazy.SitePermissions.SCOPE_TEMPORARY,
474                 this.browser
475               );
476             }
478             // Grant permission if action is ALLOW.
479             if (promptAction.action == lazy.SitePermissions.ALLOW) {
480               this.allow();
481             } else {
482               this.cancel();
483             }
484           } else if (this.permissionKey) {
485             // TODO: Add support for permitTemporaryAllow
486             if (promptAction.action == lazy.SitePermissions.BLOCK) {
487               // Temporarily store BLOCK permissions.
488               // We don't consider subframes when storing temporary
489               // permissions on a tab, thus storing ALLOW could be exploited.
490               lazy.SitePermissions.setForPrincipal(
491                 null,
492                 this.permissionKey,
493                 promptAction.action,
494                 lazy.SitePermissions.SCOPE_TEMPORARY,
495                 this.browser
496               );
497             }
498           }
499         },
500       };
501       if (promptAction.dismiss) {
502         action.dismiss = promptAction.dismiss;
503       }
505       popupNotificationActions.push(action);
506     }
508     this.#showNotification(popupNotificationActions);
509   }
511   postPrompt() {
512     let browser = this.browser;
513     let principal = this.principal;
514     let chromeWin = browser.ownerGlobal;
515     if (!chromeWin.PopupNotifications) {
516       return;
517     }
519     if (!this.permissionKey) {
520       throw new Error("permissionKey is required to show a post-prompt");
521     }
523     if (!this.postPromptActions) {
524       throw new Error("postPromptActions are required to show a post-prompt");
525     }
527     // Transform the PermissionPrompt actions into PopupNotification actions.
528     let popupNotificationActions = [];
529     for (let promptAction of this.postPromptActions) {
530       let action = {
531         label: promptAction.label,
532         accessKey: promptAction.accessKey,
533         callback: state => {
534           if (promptAction.callback) {
535             promptAction.callback();
536           }
538           // Post-prompt permissions are stored permanently by default.
539           // Since we can not reply to the original permission request anymore,
540           // the page will need to listen for permission changes which are triggered
541           // by permanent entries in the permission manager.
542           let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
543           // Only remember permission for session if in PB mode.
544           if (lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)) {
545             scope = lazy.SitePermissions.SCOPE_SESSION;
546           }
547           lazy.SitePermissions.setForPrincipal(
548             principal,
549             this.permissionKey,
550             promptAction.action,
551             scope
552           );
553         },
554       };
555       popupNotificationActions.push(action);
556     }
558     // Post-prompt animation
559     if (!chromeWin.gReduceMotion) {
560       let anchor = chromeWin.document.getElementById(this.anchorID);
561       // Only show the animation on the first request, not after e.g. tab switching.
562       anchor.addEventListener(
563         "animationend",
564         () => anchor.removeAttribute("animate"),
565         { once: true }
566       );
567       anchor.setAttribute("animate", "true");
568     }
570     this.#showNotification(popupNotificationActions, true);
571   }
573   #showNotification(actions, postPrompt = false) {
574     let chromeWin = this.browser.ownerGlobal;
575     let mainAction = actions.length ? actions[0] : null;
576     let secondaryActions = actions.splice(1);
578     let options = this.popupOptions;
580     if (!options.hasOwnProperty("displayURI") || options.displayURI) {
581       options.displayURI = this.principal.URI;
582     }
584     if (!postPrompt) {
585       // Permission prompts are always persistent; the close button is controlled by a pref.
586       options.persistent = true;
587       options.hideClose = true;
588     }
590     options.eventCallback = (topic, nextRemovalReason, isCancel) => {
591       // When the docshell of the browser is aboout to be swapped to another one,
592       // the "swapping" event is called. Returning true causes the notification
593       // to be moved to the new browser.
594       if (topic == "swapping") {
595         return true;
596       }
597       // The prompt has been shown, notify the PermissionUI.
598       // onShown() is currently not called for post-prompts,
599       // because there is no prompt that would make use of this.
600       // You can remove this restriction if you need it, but be
601       // mindful of other consumers.
602       if (topic == "shown" && !postPrompt) {
603         this.onShown();
604       }
605       // The prompt has been removed, notify the PermissionUI.
606       // onAfterShow() is currently not called for post-prompts,
607       // because there is no prompt that would make use of this.
608       // You can remove this restriction if you need it, but be
609       // mindful of other consumers.
610       if (topic == "removed" && !postPrompt) {
611         if (isCancel) {
612           this.cancel();
613         }
614         this.onAfterShow();
615       }
616       return false;
617     };
619     // Post-prompts show up as dismissed.
620     options.dismissed = postPrompt;
622     // onBeforeShow() is currently not called for post-prompts,
623     // because there is no prompt that would make use of this.
624     // You can remove this restriction if you need it, but be
625     // mindful of other consumers.
626     if (postPrompt || this.onBeforeShow() !== false) {
627       chromeWin.PopupNotifications.show(
628         this.browser,
629         this.notificationID,
630         this.message,
631         this.anchorID,
632         mainAction,
633         secondaryActions,
634         options
635       );
636     }
637   }
641  * A subclass of PermissionPrompt that assumes
642  * that this.request is an nsIContentPermissionRequest
643  * and fills in some of the required properties on the
644  * PermissionPrompt. For callers that are wrapping an
645  * nsIContentPermissionRequest, this should be subclassed
646  * rather than PermissionPrompt.
647  */
648 class PermissionPromptForRequest extends PermissionPrompt {
649   get browser() {
650     // In the e10s-case, the <xul:browser> will be at request.element.
651     // In the single-process case, we have to use some XPCOM incantations
652     // to resolve to the <xul:browser>.
653     if (this.request.element) {
654       return this.request.element;
655     }
656     return this.request.window.docShell.chromeEventHandler;
657   }
659   get principal() {
660     let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest);
661     return request.getDelegatePrincipal(this.type);
662   }
664   cancel() {
665     this.request.cancel();
666   }
668   allow(choices) {
669     this.request.allow(choices);
670   }
674  * A subclass of PermissionPromptForRequest that prompts
675  * for a Synthetic SitePermsAddon addon type and starts a synthetic
676  * addon install flow.
677  */
678 class SitePermsAddonInstallRequest extends PermissionPromptForRequest {
679   prompt() {
680     // fallback to regular permission prompt for localhost,
681     // or when the SitePermsAddonProvider is not enabled.
682     if (this.principal.isLoopbackHost || !lazy.sitePermsAddonsProviderEnabled) {
683       super.prompt();
684       return;
685     }
687     // Otherwise, we'll use the addon install flow.
688     lazy.AddonManager.installSitePermsAddonFromWebpage(
689       this.browser,
690       this.principal,
691       this.permName
692     ).then(
693       () => {
694         this.allow();
695       },
696       err => {
697         this.cancel();
699         // Print an error message in the console to give more information to the developer.
700         let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
701         let errorMessage =
702           this.getInstallErrorMessage(err) ||
703           `${this.permName} access was rejected: ${err.message}`;
705         let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
706         scriptError.initWithWindowID(
707           errorMessage,
708           null,
709           null,
710           0,
711           0,
712           0,
713           "content javascript",
714           this.browser.browsingContext.currentWindowGlobal.innerWindowId
715         );
716         Services.console.logMessage(scriptError);
717       }
718     );
719   }
721   /**
722    * Returns an error message that will be printed to the console given a passed Component.Exception.
723    * This should be overriden by children classes.
724    *
725    * @param {Components.Exception} err
726    * @returns {String} The error message
727    */
728   getInstallErrorMessage(err) {
729     return null;
730   }
734  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
735  * the GeoLocation API.
737  * @param request (nsIContentPermissionRequest)
738  *        The request for a permission from content.
739  */
740 class GeolocationPermissionPrompt extends PermissionPromptForRequest {
741   constructor(request) {
742     super();
743     this.request = request;
744   }
746   get type() {
747     return "geo";
748   }
750   get permissionKey() {
751     return "geo";
752   }
754   get popupOptions() {
755     let pref = "browser.geolocation.warning.infoURL";
756     let options = {
757       learnMoreURL: Services.urlFormatter.formatURLPref(pref),
758       displayURI: false,
759       name: this.getPrincipalName(),
760     };
762     // Don't offer "always remember" action in PB mode
763     options.checkbox = {
764       show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
765         this.browser.ownerGlobal
766       ),
767     };
769     if (this.request.isRequestDelegatedToUnsafeThirdParty) {
770       // Second name should be the third party origin
771       options.secondName = this.getPrincipalName(this.request.principal);
772       options.checkbox = { show: false };
773     }
775     if (options.checkbox.show) {
776       options.checkbox.label = lazy.gBrowserBundle.GetStringFromName(
777         "geolocation.remember"
778       );
779     }
781     return options;
782   }
784   get notificationID() {
785     return "geolocation";
786   }
788   get anchorID() {
789     return "geo-notification-icon";
790   }
792   get message() {
793     if (this.principal.schemeIs("file")) {
794       return lazy.gBrowserBundle.GetStringFromName(
795         "geolocation.shareWithFile4"
796       );
797     }
799     if (this.request.isRequestDelegatedToUnsafeThirdParty) {
800       return lazy.gBrowserBundle.formatStringFromName(
801         "geolocation.shareWithSiteUnsafeDelegation2",
802         ["<>", "{}"]
803       );
804     }
806     return lazy.gBrowserBundle.formatStringFromName(
807       "geolocation.shareWithSite4",
808       ["<>"]
809     );
810   }
812   get promptActions() {
813     return [
814       {
815         label: lazy.gBrowserBundle.GetStringFromName("geolocation.allow"),
816         accessKey: lazy.gBrowserBundle.GetStringFromName(
817           "geolocation.allow.accesskey"
818         ),
819         action: lazy.SitePermissions.ALLOW,
820       },
821       {
822         label: lazy.gBrowserBundle.GetStringFromName("geolocation.block"),
823         accessKey: lazy.gBrowserBundle.GetStringFromName(
824           "geolocation.block.accesskey"
825         ),
826         action: lazy.SitePermissions.BLOCK,
827       },
828     ];
829   }
831   #updateGeoSharing(state) {
832     let gBrowser = this.browser.ownerGlobal.gBrowser;
833     if (gBrowser == null) {
834       return;
835     }
836     gBrowser.updateBrowserSharing(this.browser, { geo: state });
838     // Update last access timestamp
839     let host;
840     try {
841       host = this.browser.currentURI.host;
842     } catch (e) {
843       return;
844     }
845     if (host == null || host == "") {
846       return;
847     }
848     lazy.ContentPrefService2.set(
849       this.browser.currentURI.host,
850       "permissions.geoLocation.lastAccess",
851       new Date().toString(),
852       this.browser.loadContext
853     );
854   }
856   allow(...args) {
857     this.#updateGeoSharing(true);
858     super.allow(...args);
859   }
861   cancel(...args) {
862     this.#updateGeoSharing(false);
863     super.cancel(...args);
864   }
868  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
869  * the WebXR API.
871  * @param request (nsIContentPermissionRequest)
872  *        The request for a permission from content.
873  */
874 class XRPermissionPrompt extends PermissionPromptForRequest {
875   constructor(request) {
876     super();
877     this.request = request;
878   }
880   get type() {
881     return "xr";
882   }
884   get permissionKey() {
885     return "xr";
886   }
888   get popupOptions() {
889     let pref = "browser.xr.warning.infoURL";
890     let options = {
891       learnMoreURL: Services.urlFormatter.formatURLPref(pref),
892       displayURI: false,
893       name: this.getPrincipalName(),
894     };
896     // Don't offer "always remember" action in PB mode
897     options.checkbox = {
898       show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
899         this.browser.ownerGlobal
900       ),
901     };
903     if (options.checkbox.show) {
904       options.checkbox.label =
905         lazy.gBrowserBundle.GetStringFromName("xr.remember");
906     }
908     return options;
909   }
911   get notificationID() {
912     return "xr";
913   }
915   get anchorID() {
916     return "xr-notification-icon";
917   }
919   get message() {
920     if (this.principal.schemeIs("file")) {
921       return lazy.gBrowserBundle.GetStringFromName("xr.shareWithFile4");
922     }
924     return lazy.gBrowserBundle.formatStringFromName("xr.shareWithSite4", [
925       "<>",
926     ]);
927   }
929   get promptActions() {
930     return [
931       {
932         label: lazy.gBrowserBundle.GetStringFromName("xr.allow2"),
933         accessKey: lazy.gBrowserBundle.GetStringFromName("xr.allow2.accesskey"),
934         action: lazy.SitePermissions.ALLOW,
935       },
936       {
937         label: lazy.gBrowserBundle.GetStringFromName("xr.block"),
938         accessKey: lazy.gBrowserBundle.GetStringFromName("xr.block.accesskey"),
939         action: lazy.SitePermissions.BLOCK,
940       },
941     ];
942   }
944   #updateXRSharing(state) {
945     let gBrowser = this.browser.ownerGlobal.gBrowser;
946     if (gBrowser == null) {
947       return;
948     }
949     gBrowser.updateBrowserSharing(this.browser, { xr: state });
951     let devicePermOrigins = this.browser.getDevicePermissionOrigins("xr");
952     if (!state) {
953       devicePermOrigins.delete(this.principal.origin);
954       return;
955     }
956     devicePermOrigins.add(this.principal.origin);
957   }
959   allow(...args) {
960     this.#updateXRSharing(true);
961     super.allow(...args);
962   }
964   cancel(...args) {
965     this.#updateXRSharing(false);
966     super.cancel(...args);
967   }
971  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
972  * the Desktop Notification API.
974  * @param request (nsIContentPermissionRequest)
975  *        The request for a permission from content.
976  * @return {PermissionPrompt} (see documentation in header)
977  */
978 class DesktopNotificationPermissionPrompt extends PermissionPromptForRequest {
979   constructor(request) {
980     super();
981     this.request = request;
983     XPCOMUtils.defineLazyPreferenceGetter(
984       this,
985       "requiresUserInput",
986       "dom.webnotifications.requireuserinteraction"
987     );
988     XPCOMUtils.defineLazyPreferenceGetter(
989       this,
990       "postPromptEnabled",
991       "permissions.desktop-notification.postPrompt.enabled"
992     );
993     XPCOMUtils.defineLazyPreferenceGetter(
994       this,
995       "notNowEnabled",
996       "permissions.desktop-notification.notNow.enabled"
997     );
998   }
1000   get type() {
1001     return "desktop-notification";
1002   }
1004   get permissionKey() {
1005     return "desktop-notification";
1006   }
1008   get popupOptions() {
1009     let learnMoreURL =
1010       Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
1012     return {
1013       learnMoreURL,
1014       displayURI: false,
1015       name: this.getPrincipalName(),
1016     };
1017   }
1019   get notificationID() {
1020     return "web-notifications";
1021   }
1023   get anchorID() {
1024     return "web-notifications-notification-icon";
1025   }
1027   get message() {
1028     return lazy.gBrowserBundle.formatStringFromName(
1029       "webNotifications.receiveFromSite3",
1030       ["<>"]
1031     );
1032   }
1034   get promptActions() {
1035     let actions = [
1036       {
1037         label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
1038         accessKey: lazy.gBrowserBundle.GetStringFromName(
1039           "webNotifications.allow2.accesskey"
1040         ),
1041         action: lazy.SitePermissions.ALLOW,
1042         scope: lazy.SitePermissions.SCOPE_PERSISTENT,
1043       },
1044     ];
1045     if (this.notNowEnabled) {
1046       actions.push({
1047         label: lazy.gBrowserBundle.GetStringFromName("webNotifications.notNow"),
1048         accessKey: lazy.gBrowserBundle.GetStringFromName(
1049           "webNotifications.notNow.accesskey"
1050         ),
1051         action: lazy.SitePermissions.BLOCK,
1052       });
1053     }
1055     let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
1056       this.browser
1057     );
1058     actions.push({
1059       label: isBrowserPrivate
1060         ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
1061         : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
1062       accessKey: isBrowserPrivate
1063         ? lazy.gBrowserBundle.GetStringFromName(
1064             "webNotifications.block.accesskey"
1065           )
1066         : lazy.gBrowserBundle.GetStringFromName(
1067             "webNotifications.alwaysBlock.accesskey"
1068           ),
1069       action: lazy.SitePermissions.BLOCK,
1070       scope: isBrowserPrivate
1071         ? lazy.SitePermissions.SCOPE_SESSION
1072         : lazy.SitePermissions.SCOPE_PERSISTENT,
1073     });
1074     return actions;
1075   }
1077   get postPromptActions() {
1078     let actions = [
1079       {
1080         label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
1081         accessKey: lazy.gBrowserBundle.GetStringFromName(
1082           "webNotifications.allow2.accesskey"
1083         ),
1084         action: lazy.SitePermissions.ALLOW,
1085       },
1086     ];
1088     let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
1089       this.browser
1090     );
1091     actions.push({
1092       label: isBrowserPrivate
1093         ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
1094         : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
1095       accessKey: isBrowserPrivate
1096         ? lazy.gBrowserBundle.GetStringFromName(
1097             "webNotifications.block.accesskey"
1098           )
1099         : lazy.gBrowserBundle.GetStringFromName(
1100             "webNotifications.alwaysBlock.accesskey"
1101           ),
1102       action: lazy.SitePermissions.BLOCK,
1103     });
1104     return actions;
1105   }
1109  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
1110  * the persistent-storage API.
1112  * @param request (nsIContentPermissionRequest)
1113  *        The request for a permission from content.
1114  */
1115 class PersistentStoragePermissionPrompt extends PermissionPromptForRequest {
1116   constructor(request) {
1117     super();
1118     this.request = request;
1119   }
1121   get type() {
1122     return "persistent-storage";
1123   }
1125   get permissionKey() {
1126     return "persistent-storage";
1127   }
1129   get popupOptions() {
1130     let learnMoreURL =
1131       Services.urlFormatter.formatURLPref("app.support.baseURL") +
1132       "storage-permissions";
1133     return {
1134       learnMoreURL,
1135       displayURI: false,
1136       name: this.getPrincipalName(),
1137     };
1138   }
1140   get notificationID() {
1141     return "persistent-storage";
1142   }
1144   get anchorID() {
1145     return "persistent-storage-notification-icon";
1146   }
1148   get message() {
1149     return lazy.gBrowserBundle.formatStringFromName(
1150       "persistentStorage.allowWithSite2",
1151       ["<>"]
1152     );
1153   }
1155   get promptActions() {
1156     return [
1157       {
1158         label: lazy.gBrowserBundle.GetStringFromName("persistentStorage.allow"),
1159         accessKey: lazy.gBrowserBundle.GetStringFromName(
1160           "persistentStorage.allow.accesskey"
1161         ),
1162         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1163         scope: lazy.SitePermissions.SCOPE_PERSISTENT,
1164       },
1165       {
1166         label: lazy.gBrowserBundle.GetStringFromName(
1167           "persistentStorage.block.label"
1168         ),
1169         accessKey: lazy.gBrowserBundle.GetStringFromName(
1170           "persistentStorage.block.accesskey"
1171         ),
1172         action: lazy.SitePermissions.BLOCK,
1173       },
1174     ];
1175   }
1179  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
1180  * the WebMIDI API.
1182  * @param request (nsIContentPermissionRequest)
1183  *        The request for a permission from content.
1184  */
1185 class MIDIPermissionPrompt extends SitePermsAddonInstallRequest {
1186   constructor(request) {
1187     super();
1188     this.request = request;
1189     let types = request.types.QueryInterface(Ci.nsIArray);
1190     let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
1191     this.isSysexPerm =
1192       !!perm.options.length &&
1193       perm.options.queryElementAt(0, Ci.nsISupportsString) == "sysex";
1194     this.permName = "midi";
1195     if (this.isSysexPerm) {
1196       this.permName = "midi-sysex";
1197     }
1198   }
1200   get type() {
1201     return "midi";
1202   }
1204   get permissionKey() {
1205     return this.permName;
1206   }
1208   get popupOptions() {
1209     // TODO (bug 1433235) We need a security/permissions explanation URL for this
1210     let options = {
1211       displayURI: false,
1212       name: this.getPrincipalName(),
1213     };
1215     // Don't offer "always remember" action in PB mode
1216     options.checkbox = {
1217       show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
1218         this.browser.ownerGlobal
1219       ),
1220     };
1222     if (options.checkbox.show) {
1223       options.checkbox.label =
1224         lazy.gBrowserBundle.GetStringFromName("midi.remember");
1225     }
1227     return options;
1228   }
1230   get notificationID() {
1231     return "midi";
1232   }
1234   get anchorID() {
1235     return "midi-notification-icon";
1236   }
1238   get message() {
1239     let message;
1240     if (this.principal.schemeIs("file")) {
1241       if (this.isSysexPerm) {
1242         message = lazy.gBrowserBundle.GetStringFromName(
1243           "midi.shareSysexWithFile"
1244         );
1245       } else {
1246         message = lazy.gBrowserBundle.GetStringFromName("midi.shareWithFile");
1247       }
1248     } else if (this.isSysexPerm) {
1249       message = lazy.gBrowserBundle.formatStringFromName(
1250         "midi.shareSysexWithSite",
1251         ["<>"]
1252       );
1253     } else {
1254       message = lazy.gBrowserBundle.formatStringFromName("midi.shareWithSite", [
1255         "<>",
1256       ]);
1257     }
1258     return message;
1259   }
1261   get promptActions() {
1262     return [
1263       {
1264         label: lazy.gBrowserBundle.GetStringFromName("midi.allow.label"),
1265         accessKey: lazy.gBrowserBundle.GetStringFromName(
1266           "midi.allow.accesskey"
1267         ),
1268         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1269       },
1270       {
1271         label: lazy.gBrowserBundle.GetStringFromName("midi.block.label"),
1272         accessKey: lazy.gBrowserBundle.GetStringFromName(
1273           "midi.block.accesskey"
1274         ),
1275         action: Ci.nsIPermissionManager.DENY_ACTION,
1276       },
1277     ];
1278   }
1280   /**
1281    * @override
1282    * @param {Components.Exception} err
1283    * @returns {String}
1284    */
1285   getInstallErrorMessage(err) {
1286     return `WebMIDI access request was denied: ❝${err.message}❞. See https://developer.mozilla.org/docs/Web/API/Navigator/requestMIDIAccess for more information`;
1287   }
1290 class StorageAccessPermissionPrompt extends PermissionPromptForRequest {
1291   #permissionKey;
1293   constructor(request) {
1294     super();
1295     this.request = request;
1296     this.siteOption = null;
1297     this.#permissionKey = `3rdPartyStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.origin}`;
1299     let types = this.request.types.QueryInterface(Ci.nsIArray);
1300     let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
1301     let options = perm.options.QueryInterface(Ci.nsIArray);
1302     // If we have an option, the permission request is different in some way.
1303     // We may be in a call from requestStorageAccessUnderSite or a frame-scoped
1304     // request, which means that the embedding principal is not the current top-level
1305     // or the permission key is different.
1306     if (options.length != 2) {
1307       return;
1308     }
1310     let topLevelOption = options.queryElementAt(0, Ci.nsISupportsString).data;
1311     if (topLevelOption) {
1312       this.siteOption = topLevelOption;
1313     }
1314     let frameOption = options.queryElementAt(1, Ci.nsISupportsString).data;
1315     if (frameOption) {
1316       // We replace the permission key with a frame-specific one that only has a site after the delimiter
1317       this.#permissionKey = `3rdPartyFrameStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.siteOrigin}`;
1318     }
1319   }
1321   get usePermissionManager() {
1322     return false;
1323   }
1325   get type() {
1326     return "storage-access";
1327   }
1329   get permissionKey() {
1330     // Make sure this name is unique per each third-party tracker
1331     return this.#permissionKey;
1332   }
1334   get temporaryPermissionURI() {
1335     if (this.siteOption) {
1336       return Services.io.newURI(this.siteOption);
1337     }
1338     return undefined;
1339   }
1341   prettifyHostPort(hostport) {
1342     let [host, port] = hostport.split(":");
1343     host = lazy.IDNService.convertToDisplayIDN(host, {});
1344     if (port) {
1345       return `${host}:${port}`;
1346     }
1347     return host;
1348   }
1350   get popupOptions() {
1351     let learnMoreURL =
1352       Services.urlFormatter.formatURLPref("app.support.baseURL") +
1353       "third-party-cookies";
1354     let hostPort = this.prettifyHostPort(this.principal.hostPort);
1355     let hintText = lazy.gBrowserBundle.formatStringFromName(
1356       "storageAccess1.hintText",
1357       [hostPort]
1358     );
1359     return {
1360       learnMoreURL,
1361       displayURI: false,
1362       hintText,
1363       escAction: "secondarybuttoncommand",
1364     };
1365   }
1367   get notificationID() {
1368     return "storage-access";
1369   }
1371   get anchorID() {
1372     return "storage-access-notification-icon";
1373   }
1375   get message() {
1376     let embeddingHost = this.topLevelPrincipal.host;
1378     if (this.siteOption) {
1379       embeddingHost = this.siteOption.split("://").at(-1);
1380     }
1382     return lazy.gBrowserBundle.formatStringFromName("storageAccess4.message", [
1383       this.prettifyHostPort(this.principal.hostPort),
1384       this.prettifyHostPort(embeddingHost),
1385     ]);
1386   }
1388   get promptActions() {
1389     let self = this;
1391     return [
1392       {
1393         label: lazy.gBrowserBundle.GetStringFromName(
1394           "storageAccess1.Allow.label"
1395         ),
1396         accessKey: lazy.gBrowserBundle.GetStringFromName(
1397           "storageAccess1.Allow.accesskey"
1398         ),
1399         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1400         callback(state) {
1401           self.allow({ "storage-access": "allow" });
1402         },
1403       },
1404       {
1405         label: lazy.gBrowserBundle.GetStringFromName(
1406           "storageAccess1.DontAllow.label"
1407         ),
1408         accessKey: lazy.gBrowserBundle.GetStringFromName(
1409           "storageAccess1.DontAllow.accesskey"
1410         ),
1411         action: Ci.nsIPermissionManager.DENY_ACTION,
1412         callback(state) {
1413           self.cancel();
1414         },
1415       },
1416     ];
1417   }
1419   get topLevelPrincipal() {
1420     return this.request.topLevelPrincipal;
1421   }
1424 export const PermissionUI = {
1425   PermissionPromptForRequest,
1426   GeolocationPermissionPrompt,
1427   XRPermissionPrompt,
1428   DesktopNotificationPermissionPrompt,
1429   PersistentStoragePermissionPrompt,
1430   MIDIPermissionPrompt,
1431   StorageAccessPermissionPrompt,