Bug 1731264: update documentation about PopupNotifications in PermissionsUI.jsm....
[gecko.git] / browser / modules / PermissionUI.jsm
blobe7df6ef09a18f09ee4c7894173bb97b15d3854f8
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 "use strict";
7 var EXPORTED_SYMBOLS = ["PermissionUI"];
9 /**
10  * PermissionUI is responsible for exposing both a prototype
11  * PermissionPrompt that can be used by arbitrary browser
12  * components and add-ons, but also hosts the implementations of
13  * built-in permission prompts.
14  *
15  * If you're developing a feature that requires web content to ask
16  * for special permissions from the user, this module is for you.
17  *
18  * Suppose a system add-on wants to add a new prompt for a new request
19  * for getting more low-level access to the user's sound card, and the
20  * permission request is coming up from content by way of the
21  * nsContentPermissionHelper. The system add-on could then do the following:
22  *
23  * Cu.import("resource://gre/modules/Integration.jsm");
24  * Cu.import("resource:///modules/PermissionUI.jsm");
25  *
26  * const SoundCardIntegration = (base) => ({
27  *   __proto__: base,
28  *   createPermissionPrompt(type, request) {
29  *     if (type != "sound-api") {
30  *       return super.createPermissionPrompt(...arguments);
31  *     }
32  *
33  *     return {
34  *       __proto__: PermissionUI.PermissionPromptForRequestPrototype,
35  *       get permissionKey() {
36  *         return "sound-permission";
37  *       }
38  *       // etc - see the documentation for PermissionPrompt for
39  *       // a better idea of what things one can and should override.
40  *     }
41  *   },
42  * });
43  *
44  * // Add-on startup:
45  * Integration.contentPermission.register(SoundCardIntegration);
46  * // ...
47  * // Add-on shutdown:
48  * Integration.contentPermission.unregister(SoundCardIntegration);
49  *
50  * Note that PermissionPromptForRequestPrototype must be used as the
51  * prototype, since the prompt is wrapping an nsIContentPermissionRequest,
52  * and going through nsIContentPermissionPrompt.
53  *
54  * It is, however, possible to take advantage of PermissionPrompt without
55  * having to go through nsIContentPermissionPrompt or with a
56  * nsIContentPermissionRequest. The PermissionPromptPrototype can be
57  * imported, subclassed, and have prompt() called directly, without
58  * the caller having called into createPermissionPrompt.
59  */
60 const { XPCOMUtils } = ChromeUtils.import(
61   "resource://gre/modules/XPCOMUtils.jsm"
64 ChromeUtils.defineModuleGetter(
65   this,
66   "Services",
67   "resource://gre/modules/Services.jsm"
69 ChromeUtils.defineModuleGetter(
70   this,
71   "SitePermissions",
72   "resource:///modules/SitePermissions.jsm"
74 ChromeUtils.defineModuleGetter(
75   this,
76   "PrivateBrowsingUtils",
77   "resource://gre/modules/PrivateBrowsingUtils.jsm"
80 XPCOMUtils.defineLazyServiceGetter(
81   this,
82   "IDNService",
83   "@mozilla.org/network/idn-service;1",
84   "nsIIDNService"
87 XPCOMUtils.defineLazyServiceGetter(
88   this,
89   "ContentPrefService2",
90   "@mozilla.org/content-pref/service;1",
91   "nsIContentPrefService2"
94 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
95   return Services.strings.createBundle(
96     "chrome://browser/locale/browser.properties"
97   );
98 });
100 var PermissionUI = {};
103  * PermissionPromptPrototype should be subclassed by callers that
104  * want to display prompts to the user. See each method and property
105  * below for guidance on what to override.
107  * Note that if you're creating a prompt for an
108  * nsIContentPermissionRequest, you'll want to subclass
109  * PermissionPromptForRequestPrototype instead.
110  */
111 var PermissionPromptPrototype = {
112   /**
113    * Returns the associated <xul:browser> for the request. This should
114    * work for the e10s and non-e10s case.
115    *
116    * Subclasses must override this.
117    *
118    * @return {<xul:browser>}
119    */
120   get browser() {
121     throw new Error("Not implemented.");
122   },
124   /**
125    * Returns the nsIPrincipal associated with the request.
126    *
127    * Subclasses must override this.
128    *
129    * @return {nsIPrincipal}
130    */
131   get principal() {
132     throw new Error("Not implemented.");
133   },
135   /**
136    * Indicates the type of the permission request from content. This type might
137    * be different from the permission key used in the permissions database.
138    */
139   get type() {
140     return undefined;
141   },
143   /**
144    * If the nsIPermissionManager is being queried and written
145    * to for this permission request, set this to the key to be
146    * used. If this is undefined, no integration with temporary
147    * permissions infrastructure will be provided.
148    *
149    * Note that if a permission is set, in any follow-up
150    * prompting within the expiry window of that permission,
151    * the prompt will be skipped and the allow or deny choice
152    * will be selected automatically.
153    */
154   get permissionKey() {
155     return undefined;
156   },
158   /**
159    * If true, user permissions will be read from and written to.
160    * When this is false, we still provide integration with
161    * infrastructure such as temporary permissions. permissionKey should
162    * still return a valid name in those cases for that integration to work.
163    */
164   get usePermissionManager() {
165     return true;
166   },
168   /**
169    * These are the options that will be passed to the PopupNotification when it
170    * is shown. See the documentation of `PopupNotifications_show` in
171    * PopupNotifications.jsm for details.
172    *
173    * Note that prompt() will automatically set displayURI to
174    * be the URI of the requesting pricipal, unless the displayURI is exactly
175    * set to false.
176    */
177   get popupOptions() {
178     return {};
179   },
181   /**
182    * If true, automatically denied permission requests will
183    * spawn a "post-prompt" that allows the user to correct the
184    * automatic denial by giving permanent permission access to
185    * the site.
186    *
187    * Note that if this function returns true, the permissionKey
188    * and postPromptActions attributes must be implemented.
189    */
190   get postPromptEnabled() {
191     return false;
192   },
194   /**
195    * If true, the prompt will be cancelled automatically unless
196    * request.isHandlingUserInput is true.
197    */
198   get requiresUserInput() {
199     return false;
200   },
202   /**
203    * PopupNotification requires a unique ID to open the notification.
204    * You must return a unique ID string here, for which PopupNotification
205    * will then create a <xul:popupnotification> node with the ID
206    * "<notificationID>-notification".
207    *
208    * If there's a custom <xul:popupnotification> you're hoping to show,
209    * then you need to make sure its ID has the "-notification" suffix,
210    * and then return the prefix here.
211    *
212    * See PopupNotifications.jsm for more details.
213    *
214    * @return {string}
215    *         The unique ID that will be used to as the
216    *         "<unique ID>-notification" ID for the <xul:popupnotification>
217    *         to use or create.
218    */
219   get notificationID() {
220     throw new Error("Not implemented.");
221   },
223   /**
224    * The ID of the element to anchor the PopupNotification to.
225    *
226    * @return {string}
227    */
228   get anchorID() {
229     return "default-notification-icon";
230   },
232   /**
233    * The message to show to the user in the PopupNotification, see
234    * `PopupNotifications_show` in PopupNotifications.jsm.
235    *
236    * Subclasses must override this.
237    *
238    * @return {string}
239    */
240   get message() {
241     throw new Error("Not implemented.");
242   },
244   /**
245    * Provides the preferred name to use in the permission popups,
246    * based on the principal URI (the URI.hostPort for any URI scheme
247    * besides the moz-extension one which should default to the
248    * extension name).
249    */
250   getPrincipalName(principal = this.principal) {
251     if (principal.addonPolicy) {
252       return principal.addonPolicy.name;
253     }
255     return principal.hostPort;
256   },
258   /**
259    * This will be called if the request is to be cancelled.
260    *
261    * Subclasses only need to override this if they provide a
262    * permissionKey.
263    */
264   cancel() {
265     throw new Error("Not implemented.");
266   },
268   /**
269    * This will be called if the request is to be allowed.
270    *
271    * Subclasses only need to override this if they provide a
272    * permissionKey.
273    */
274   allow() {
275     throw new Error("Not implemented.");
276   },
278   /**
279    * The actions that will be displayed in the PopupNotification
280    * via a dropdown menu. The first item in this array will be
281    * the default selection. Each action is an Object with the
282    * following properties:
283    *
284    *  label (string):
285    *    The label that will be displayed for this choice.
286    *  accessKey (string):
287    *    The access key character that will be used for this choice.
288    *  action (SitePermissions state)
289    *    The action that will be associated with this choice.
290    *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
291    *  scope (SitePermissions scope)
292    *    The scope of the associated action (e.g. SitePermissions.SCOPE_PERSISTENT)
293    *
294    *  callback (function, optional)
295    *    A callback function that will fire if the user makes this choice, with
296    *    a single parameter, state. State is an Object that contains the property
297    *    checkboxChecked, which identifies whether the checkbox to remember this
298    *    decision was checked.
299    */
300   get promptActions() {
301     return [];
302   },
304   /**
305    * The actions that will be displayed in the PopupNotification
306    * for post-prompt notifications via a dropdown menu.
307    * The first item in this array will be the default selection.
308    * Each action is an Object with the following properties:
309    *
310    *  label (string):
311    *    The label that will be displayed for this choice.
312    *  accessKey (string):
313    *    The access key character that will be used for this choice.
314    *  action (SitePermissions state)
315    *    The action that will be associated with this choice.
316    *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
317    *    Note that the scope of this action will always be persistent.
318    *
319    *  callback (function, optional)
320    *    A callback function that will fire if the user makes this choice.
321    */
322   get postPromptActions() {
323     return null;
324   },
326   /**
327    * If the prompt will be shown to the user, this callback will
328    * be called just before. Subclasses may want to override this
329    * in order to, for example, bump a counter Telemetry probe for
330    * how often a particular permission request is seen.
331    *
332    * If this returns false, it cancels the process of showing the prompt.  In
333    * that case, it is the responsibility of the onBeforeShow() implementation
334    * to ensure that allow() or cancel() are called on the object appropriately.
335    */
336   onBeforeShow() {
337     return true;
338   },
340   /**
341    * If the prompt was shown to the user, this callback will be called just
342    * after it's been shown.
343    */
344   onShown() {},
346   /**
347    * If the prompt was shown to the user, this callback will be called just
348    * after it's been hidden.
349    */
350   onAfterShow() {},
352   /**
353    * Will determine if a prompt should be shown to the user, and if so,
354    * will show it.
355    *
356    * If a permissionKey is defined prompt() might automatically
357    * allow or cancel itself based on the user's current
358    * permission settings without displaying the prompt.
359    *
360    * If the permission is not already set and the <xul:browser> that the request
361    * is associated with does not belong to a browser window with the
362    * PopupNotifications global set, the prompt request is ignored.
363    */
364   prompt() {
365     // We ignore requests from non-nsIStandardURLs
366     let requestingURI = this.principal.URI;
367     if (!(requestingURI instanceof Ci.nsIStandardURL)) {
368       return;
369     }
371     if (this.usePermissionManager && this.permissionKey) {
372       // If we're reading and setting permissions, then we need
373       // to check to see if we already have a permission setting
374       // for this particular principal.
375       let { state } = SitePermissions.getForPrincipal(
376         this.principal,
377         this.permissionKey,
378         this.browser
379       );
381       if (state == SitePermissions.BLOCK) {
382         // If this block was done based on a global user setting, we want to show
383         // a post prompt to give the user some more granular control without
384         // annoying them too much.
385         if (
386           this.postPromptEnabled &&
387           SitePermissions.getDefault(this.permissionKey) ==
388             SitePermissions.BLOCK
389         ) {
390           this.postPrompt();
391         }
392         this.cancel();
393         return;
394       }
396       if (
397         state == SitePermissions.ALLOW &&
398         !this.request.maybeUnsafePermissionDelegate
399       ) {
400         this.allow();
401         return;
402       }
403     } else if (this.permissionKey) {
404       // If we're reading a permission which already has a temporary value,
405       // see if we can use the temporary value.
406       let { state } = SitePermissions.getForPrincipal(
407         null,
408         this.permissionKey,
409         this.browser
410       );
412       if (state == SitePermissions.BLOCK) {
413         this.cancel();
414         return;
415       }
416     }
418     if (this.requiresUserInput && !this.request.isHandlingUserInput) {
419       if (this.postPromptEnabled) {
420         this.postPrompt();
421       }
422       this.cancel();
423       return;
424     }
426     let chromeWin = this.browser.ownerGlobal;
427     if (!chromeWin.PopupNotifications) {
428       this.cancel();
429       return;
430     }
432     // Transform the PermissionPrompt actions into PopupNotification actions.
433     let popupNotificationActions = [];
434     for (let promptAction of this.promptActions) {
435       let action = {
436         label: promptAction.label,
437         accessKey: promptAction.accessKey,
438         callback: state => {
439           if (promptAction.callback) {
440             promptAction.callback();
441           }
443           if (this.usePermissionManager && this.permissionKey) {
444             if (
445               (state && state.checkboxChecked && state.source != "esc-press") ||
446               promptAction.scope == SitePermissions.SCOPE_PERSISTENT
447             ) {
448               // Permanently store permission.
449               let scope = SitePermissions.SCOPE_PERSISTENT;
450               // Only remember permission for session if in PB mode.
451               if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
452                 scope = SitePermissions.SCOPE_SESSION;
453               }
454               SitePermissions.setForPrincipal(
455                 this.principal,
456                 this.permissionKey,
457                 promptAction.action,
458                 scope
459               );
460             } else if (promptAction.action == SitePermissions.BLOCK) {
461               // Temporarily store BLOCK permissions only
462               // SitePermissions does not consider subframes when storing temporary
463               // permissions on a tab, thus storing ALLOW could be exploited.
464               SitePermissions.setForPrincipal(
465                 this.principal,
466                 this.permissionKey,
467                 promptAction.action,
468                 SitePermissions.SCOPE_TEMPORARY,
469                 this.browser
470               );
471             }
473             // Grant permission if action is ALLOW.
474             if (promptAction.action == SitePermissions.ALLOW) {
475               this.allow();
476             } else {
477               this.cancel();
478             }
479           } else if (this.permissionKey) {
480             // TODO: Add support for permitTemporaryAllow
481             if (promptAction.action == SitePermissions.BLOCK) {
482               // Temporarily store BLOCK permissions.
483               // We don't consider subframes when storing temporary
484               // permissions on a tab, thus storing ALLOW could be exploited.
485               SitePermissions.setForPrincipal(
486                 null,
487                 this.permissionKey,
488                 promptAction.action,
489                 SitePermissions.SCOPE_TEMPORARY,
490                 this.browser
491               );
492             }
493           }
494         },
495       };
496       if (promptAction.dismiss) {
497         action.dismiss = promptAction.dismiss;
498       }
500       popupNotificationActions.push(action);
501     }
503     this._showNotification(popupNotificationActions);
504   },
506   postPrompt() {
507     let browser = this.browser;
508     let principal = this.principal;
509     let chromeWin = browser.ownerGlobal;
510     if (!chromeWin.PopupNotifications) {
511       return;
512     }
514     if (!this.permissionKey) {
515       throw new Error("permissionKey is required to show a post-prompt");
516     }
518     if (!this.postPromptActions) {
519       throw new Error("postPromptActions are required to show a post-prompt");
520     }
522     // Transform the PermissionPrompt actions into PopupNotification actions.
523     let popupNotificationActions = [];
524     for (let promptAction of this.postPromptActions) {
525       let action = {
526         label: promptAction.label,
527         accessKey: promptAction.accessKey,
528         callback: state => {
529           if (promptAction.callback) {
530             promptAction.callback();
531           }
533           // Post-prompt permissions are stored permanently by default.
534           // Since we can not reply to the original permission request anymore,
535           // the page will need to listen for permission changes which are triggered
536           // by permanent entries in the permission manager.
537           let scope = SitePermissions.SCOPE_PERSISTENT;
538           // Only remember permission for session if in PB mode.
539           if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
540             scope = SitePermissions.SCOPE_SESSION;
541           }
542           SitePermissions.setForPrincipal(
543             principal,
544             this.permissionKey,
545             promptAction.action,
546             scope
547           );
548         },
549       };
550       popupNotificationActions.push(action);
551     }
553     // Post-prompt animation
554     if (!chromeWin.gReduceMotion) {
555       let anchor = chromeWin.document.getElementById(this.anchorID);
556       // Only show the animation on the first request, not after e.g. tab switching.
557       anchor.addEventListener(
558         "animationend",
559         () => anchor.removeAttribute("animate"),
560         { once: true }
561       );
562       anchor.setAttribute("animate", "true");
563     }
565     this._showNotification(popupNotificationActions, true);
566   },
568   _showNotification(actions, postPrompt = false) {
569     let chromeWin = this.browser.ownerGlobal;
570     let mainAction = actions.length ? actions[0] : null;
571     let secondaryActions = actions.splice(1);
573     let options = this.popupOptions;
575     if (!options.hasOwnProperty("displayURI") || options.displayURI) {
576       options.displayURI = this.principal.URI;
577     }
579     if (!postPrompt) {
580       // Permission prompts are always persistent; the close button is controlled by a pref.
581       options.persistent = true;
582       options.hideClose = true;
583     }
585     options.eventCallback = (topic, nextRemovalReason, isCancel) => {
586       // When the docshell of the browser is aboout to be swapped to another one,
587       // the "swapping" event is called. Returning true causes the notification
588       // to be moved to the new browser.
589       if (topic == "swapping") {
590         return true;
591       }
592       // The prompt has been shown, notify the PermissionUI.
593       // onShown() is currently not called for post-prompts,
594       // because there is no prompt that would make use of this.
595       // You can remove this restriction if you need it, but be
596       // mindful of other consumers.
597       if (topic == "shown" && !postPrompt) {
598         this.onShown();
599       }
600       // The prompt has been removed, notify the PermissionUI.
601       // onAfterShow() is currently not called for post-prompts,
602       // because there is no prompt that would make use of this.
603       // You can remove this restriction if you need it, but be
604       // mindful of other consumers.
605       if (topic == "removed" && !postPrompt) {
606         if (isCancel) {
607           this.cancel();
608         }
609         this.onAfterShow();
610       }
611       return false;
612     };
614     // Post-prompts show up as dismissed.
615     options.dismissed = postPrompt;
617     // onBeforeShow() is currently not called for post-prompts,
618     // because there is no prompt that would make use of this.
619     // You can remove this restriction if you need it, but be
620     // mindful of other consumers.
621     if (postPrompt || this.onBeforeShow() !== false) {
622       chromeWin.PopupNotifications.show(
623         this.browser,
624         this.notificationID,
625         this.message,
626         this.anchorID,
627         mainAction,
628         secondaryActions,
629         options
630       );
631     }
632   },
635 PermissionUI.PermissionPromptPrototype = PermissionPromptPrototype;
638  * A subclass of PermissionPromptPrototype that assumes
639  * that this.request is an nsIContentPermissionRequest
640  * and fills in some of the required properties on the
641  * PermissionPrompt. For callers that are wrapping an
642  * nsIContentPermissionRequest, this should be subclassed
643  * rather than PermissionPromptPrototype.
644  */
645 var PermissionPromptForRequestPrototype = {
646   __proto__: PermissionPromptPrototype,
648   get browser() {
649     // In the e10s-case, the <xul:browser> will be at request.element.
650     // In the single-process case, we have to use some XPCOM incantations
651     // to resolve to the <xul:browser>.
652     if (this.request.element) {
653       return this.request.element;
654     }
655     return this.request.window.docShell.chromeEventHandler;
656   },
658   get principal() {
659     let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest);
660     return request.getDelegatePrincipal(this.type);
661   },
663   cancel() {
664     this.request.cancel();
665   },
667   allow(choices) {
668     this.request.allow(choices);
669   },
672 PermissionUI.PermissionPromptForRequestPrototype = PermissionPromptForRequestPrototype;
675  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
676  * the GeoLocation API.
678  * @param request (nsIContentPermissionRequest)
679  *        The request for a permission from content.
680  */
681 function GeolocationPermissionPrompt(request) {
682   this.request = request;
685 GeolocationPermissionPrompt.prototype = {
686   __proto__: PermissionPromptForRequestPrototype,
688   get type() {
689     return "geo";
690   },
692   get permissionKey() {
693     return "geo";
694   },
696   get popupOptions() {
697     let pref = "browser.geolocation.warning.infoURL";
698     let options = {
699       learnMoreURL: Services.urlFormatter.formatURLPref(pref),
700       displayURI: false,
701       name: this.getPrincipalName(),
702     };
704     if (this.principal.schemeIs("file")) {
705       options.checkbox = { show: false };
706     } else {
707       // Don't offer "always remember" action in PB mode
708       options.checkbox = {
709         show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal),
710       };
711     }
713     if (this.request.maybeUnsafePermissionDelegate) {
714       // Second name should be the third party origin
715       options.secondName = this.getPrincipalName(this.request.principal);
716       options.checkbox = { show: false };
717     }
719     if (options.checkbox.show) {
720       options.checkbox.label = gBrowserBundle.GetStringFromName(
721         "geolocation.remember"
722       );
723     }
725     return options;
726   },
728   get notificationID() {
729     return "geolocation";
730   },
732   get anchorID() {
733     return "geo-notification-icon";
734   },
736   get message() {
737     if (this.principal.schemeIs("file")) {
738       return gBrowserBundle.GetStringFromName("geolocation.shareWithFile4");
739     }
741     if (this.request.maybeUnsafePermissionDelegate) {
742       return gBrowserBundle.formatStringFromName(
743         "geolocation.shareWithSiteUnsafeDelegation2",
744         ["<>", "{}"]
745       );
746     }
748     return gBrowserBundle.formatStringFromName("geolocation.shareWithSite4", [
749       "<>",
750     ]);
751   },
753   get promptActions() {
754     return [
755       {
756         label: gBrowserBundle.GetStringFromName("geolocation.allow"),
757         accessKey: gBrowserBundle.GetStringFromName(
758           "geolocation.allow.accesskey"
759         ),
760         action: SitePermissions.ALLOW,
761       },
762       {
763         label: gBrowserBundle.GetStringFromName("geolocation.block"),
764         accessKey: gBrowserBundle.GetStringFromName(
765           "geolocation.block.accesskey"
766         ),
767         action: SitePermissions.BLOCK,
768       },
769     ];
770   },
772   _updateGeoSharing(state) {
773     let gBrowser = this.browser.ownerGlobal.gBrowser;
774     if (gBrowser == null) {
775       return;
776     }
777     gBrowser.updateBrowserSharing(this.browser, { geo: state });
779     // Update last access timestamp
780     let host;
781     try {
782       host = this.browser.currentURI.host;
783     } catch (e) {
784       return;
785     }
786     if (host == null || host == "") {
787       return;
788     }
789     ContentPrefService2.set(
790       this.browser.currentURI.host,
791       "permissions.geoLocation.lastAccess",
792       new Date().toString(),
793       this.browser.loadContext
794     );
795   },
797   allow(...args) {
798     this._updateGeoSharing(true);
799     PermissionPromptForRequestPrototype.allow.apply(this, args);
800   },
802   cancel(...args) {
803     this._updateGeoSharing(false);
804     PermissionPromptForRequestPrototype.cancel.apply(this, args);
805   },
808 PermissionUI.GeolocationPermissionPrompt = GeolocationPermissionPrompt;
811  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
812  * the WebXR API.
814  * @param request (nsIContentPermissionRequest)
815  *        The request for a permission from content.
816  */
817 function XRPermissionPrompt(request) {
818   this.request = request;
821 XRPermissionPrompt.prototype = {
822   __proto__: PermissionPromptForRequestPrototype,
824   get type() {
825     return "xr";
826   },
828   get permissionKey() {
829     return "xr";
830   },
832   get popupOptions() {
833     let pref = "browser.xr.warning.infoURL";
834     let options = {
835       learnMoreURL: Services.urlFormatter.formatURLPref(pref),
836       displayURI: false,
837       name: this.getPrincipalName(),
838     };
840     if (this.principal.schemeIs("file")) {
841       options.checkbox = { show: false };
842     } else {
843       // Don't offer "always remember" action in PB mode
844       options.checkbox = {
845         show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal),
846       };
847     }
849     if (options.checkbox.show) {
850       options.checkbox.label = gBrowserBundle.GetStringFromName("xr.remember");
851     }
853     return options;
854   },
856   get notificationID() {
857     return "xr";
858   },
860   get anchorID() {
861     return "xr-notification-icon";
862   },
864   get message() {
865     if (this.principal.schemeIs("file")) {
866       return gBrowserBundle.GetStringFromName("xr.shareWithFile4");
867     }
869     return gBrowserBundle.formatStringFromName("xr.shareWithSite4", ["<>"]);
870   },
872   get promptActions() {
873     return [
874       {
875         label: gBrowserBundle.GetStringFromName("xr.allow2"),
876         accessKey: gBrowserBundle.GetStringFromName("xr.allow2.accesskey"),
877         action: SitePermissions.ALLOW,
878       },
879       {
880         label: gBrowserBundle.GetStringFromName("xr.block"),
881         accessKey: gBrowserBundle.GetStringFromName("xr.block.accesskey"),
882         action: SitePermissions.BLOCK,
883       },
884     ];
885   },
887   _updateXRSharing(state) {
888     let gBrowser = this.browser.ownerGlobal.gBrowser;
889     if (gBrowser == null) {
890       return;
891     }
892     gBrowser.updateBrowserSharing(this.browser, { xr: state });
894     let devicePermOrigins = this.browser.getDevicePermissionOrigins("xr");
895     if (!state) {
896       devicePermOrigins.delete(this.principal.origin);
897       return;
898     }
899     devicePermOrigins.add(this.principal.origin);
900   },
902   allow(...args) {
903     this._updateXRSharing(true);
904     PermissionPromptForRequestPrototype.allow.apply(this, args);
905   },
907   cancel(...args) {
908     this._updateXRSharing(false);
909     PermissionPromptForRequestPrototype.cancel.apply(this, args);
910   },
913 PermissionUI.XRPermissionPrompt = XRPermissionPrompt;
916  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
917  * the Desktop Notification API.
919  * @param request (nsIContentPermissionRequest)
920  *        The request for a permission from content.
921  * @return {PermissionPrompt} (see documentation in header)
922  */
923 function DesktopNotificationPermissionPrompt(request) {
924   this.request = request;
926   XPCOMUtils.defineLazyPreferenceGetter(
927     this,
928     "requiresUserInput",
929     "dom.webnotifications.requireuserinteraction"
930   );
931   XPCOMUtils.defineLazyPreferenceGetter(
932     this,
933     "postPromptEnabled",
934     "permissions.desktop-notification.postPrompt.enabled"
935   );
936   XPCOMUtils.defineLazyPreferenceGetter(
937     this,
938     "notNowEnabled",
939     "permissions.desktop-notification.notNow.enabled"
940   );
943 DesktopNotificationPermissionPrompt.prototype = {
944   __proto__: PermissionPromptForRequestPrototype,
946   get type() {
947     return "desktop-notification";
948   },
950   get permissionKey() {
951     return "desktop-notification";
952   },
954   get popupOptions() {
955     let learnMoreURL =
956       Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
958     return {
959       learnMoreURL,
960       displayURI: false,
961       name: this.getPrincipalName(),
962     };
963   },
965   get notificationID() {
966     return "web-notifications";
967   },
969   get anchorID() {
970     return "web-notifications-notification-icon";
971   },
973   get message() {
974     return gBrowserBundle.formatStringFromName(
975       "webNotifications.receiveFromSite3",
976       ["<>"]
977     );
978   },
980   get promptActions() {
981     let actions = [
982       {
983         label: gBrowserBundle.GetStringFromName("webNotifications.allow2"),
984         accessKey: gBrowserBundle.GetStringFromName(
985           "webNotifications.allow2.accesskey"
986         ),
987         action: SitePermissions.ALLOW,
988         scope: SitePermissions.SCOPE_PERSISTENT,
989       },
990     ];
991     if (this.notNowEnabled) {
992       actions.push({
993         label: gBrowserBundle.GetStringFromName("webNotifications.notNow"),
994         accessKey: gBrowserBundle.GetStringFromName(
995           "webNotifications.notNow.accesskey"
996         ),
997         action: SitePermissions.BLOCK,
998       });
999     }
1001     let isBrowserPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
1002     actions.push({
1003       label: isBrowserPrivate
1004         ? gBrowserBundle.GetStringFromName("webNotifications.block")
1005         : gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
1006       accessKey: isBrowserPrivate
1007         ? gBrowserBundle.GetStringFromName("webNotifications.block.accesskey")
1008         : gBrowserBundle.GetStringFromName(
1009             "webNotifications.alwaysBlock.accesskey"
1010           ),
1011       action: SitePermissions.BLOCK,
1012       scope: isBrowserPrivate
1013         ? SitePermissions.SCOPE_SESSION
1014         : SitePermissions.SCOPE_PERSISTENT,
1015     });
1016     return actions;
1017   },
1019   get postPromptActions() {
1020     let actions = [
1021       {
1022         label: gBrowserBundle.GetStringFromName("webNotifications.allow2"),
1023         accessKey: gBrowserBundle.GetStringFromName(
1024           "webNotifications.allow2.accesskey"
1025         ),
1026         action: SitePermissions.ALLOW,
1027       },
1028     ];
1030     let isBrowserPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
1031     actions.push({
1032       label: isBrowserPrivate
1033         ? gBrowserBundle.GetStringFromName("webNotifications.block")
1034         : gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
1035       accessKey: isBrowserPrivate
1036         ? gBrowserBundle.GetStringFromName("webNotifications.block.accesskey")
1037         : gBrowserBundle.GetStringFromName(
1038             "webNotifications.alwaysBlock.accesskey"
1039           ),
1040       action: SitePermissions.BLOCK,
1041     });
1042     return actions;
1043   },
1046 PermissionUI.DesktopNotificationPermissionPrompt = DesktopNotificationPermissionPrompt;
1049  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
1050  * the persistent-storage API.
1052  * @param request (nsIContentPermissionRequest)
1053  *        The request for a permission from content.
1054  */
1055 function PersistentStoragePermissionPrompt(request) {
1056   this.request = request;
1059 PersistentStoragePermissionPrompt.prototype = {
1060   __proto__: PermissionPromptForRequestPrototype,
1062   get type() {
1063     return "persistent-storage";
1064   },
1066   get permissionKey() {
1067     return "persistent-storage";
1068   },
1070   get popupOptions() {
1071     let learnMoreURL =
1072       Services.urlFormatter.formatURLPref("app.support.baseURL") +
1073       "storage-permissions";
1074     return {
1075       learnMoreURL,
1076       displayURI: false,
1077       name: this.getPrincipalName(),
1078     };
1079   },
1081   get notificationID() {
1082     return "persistent-storage";
1083   },
1085   get anchorID() {
1086     return "persistent-storage-notification-icon";
1087   },
1089   get message() {
1090     return gBrowserBundle.formatStringFromName(
1091       "persistentStorage.allowWithSite2",
1092       ["<>"]
1093     );
1094   },
1096   get promptActions() {
1097     return [
1098       {
1099         label: gBrowserBundle.GetStringFromName("persistentStorage.allow"),
1100         accessKey: gBrowserBundle.GetStringFromName(
1101           "persistentStorage.allow.accesskey"
1102         ),
1103         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1104         scope: SitePermissions.SCOPE_PERSISTENT,
1105       },
1106       {
1107         label: gBrowserBundle.GetStringFromName(
1108           "persistentStorage.block.label"
1109         ),
1110         accessKey: gBrowserBundle.GetStringFromName(
1111           "persistentStorage.block.accesskey"
1112         ),
1113         action: SitePermissions.BLOCK,
1114       },
1115     ];
1116   },
1119 PermissionUI.PersistentStoragePermissionPrompt = PersistentStoragePermissionPrompt;
1122  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
1123  * the WebMIDI API.
1125  * @param request (nsIContentPermissionRequest)
1126  *        The request for a permission from content.
1127  */
1128 function MIDIPermissionPrompt(request) {
1129   this.request = request;
1130   let types = request.types.QueryInterface(Ci.nsIArray);
1131   let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
1132   this.isSysexPerm =
1133     !!perm.options.length &&
1134     perm.options.queryElementAt(0, Ci.nsISupportsString) == "sysex";
1135   this.permName = "midi";
1136   if (this.isSysexPerm) {
1137     this.permName = "midi-sysex";
1138   }
1141 MIDIPermissionPrompt.prototype = {
1142   __proto__: PermissionPromptForRequestPrototype,
1144   get type() {
1145     return "midi";
1146   },
1148   get permissionKey() {
1149     return this.permName;
1150   },
1152   get popupOptions() {
1153     // TODO (bug 1433235) We need a security/permissions explanation URL for this
1154     let options = {
1155       displayURI: false,
1156       name: this.getPrincipalName(),
1157     };
1159     if (this.principal.schemeIs("file")) {
1160       options.checkbox = { show: false };
1161     } else {
1162       // Don't offer "always remember" action in PB mode
1163       options.checkbox = {
1164         show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal),
1165       };
1166     }
1168     if (options.checkbox.show) {
1169       options.checkbox.label = gBrowserBundle.GetStringFromName(
1170         "midi.remember"
1171       );
1172     }
1174     return options;
1175   },
1177   get notificationID() {
1178     return "midi";
1179   },
1181   get anchorID() {
1182     return "midi-notification-icon";
1183   },
1185   get message() {
1186     let message;
1187     if (this.principal.schemeIs("file")) {
1188       if (this.isSysexPerm) {
1189         message = gBrowserBundle.GetStringFromName("midi.shareSysexWithFile");
1190       } else {
1191         message = gBrowserBundle.GetStringFromName("midi.shareWithFile");
1192       }
1193     } else if (this.isSysexPerm) {
1194       message = gBrowserBundle.formatStringFromName("midi.shareSysexWithSite", [
1195         "<>",
1196       ]);
1197     } else {
1198       message = gBrowserBundle.formatStringFromName("midi.shareWithSite", [
1199         "<>",
1200       ]);
1201     }
1202     return message;
1203   },
1205   get promptActions() {
1206     return [
1207       {
1208         label: gBrowserBundle.GetStringFromName("midi.allow.label"),
1209         accessKey: gBrowserBundle.GetStringFromName("midi.allow.accesskey"),
1210         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1211       },
1212       {
1213         label: gBrowserBundle.GetStringFromName("midi.block.label"),
1214         accessKey: gBrowserBundle.GetStringFromName("midi.block.accesskey"),
1215         action: Ci.nsIPermissionManager.DENY_ACTION,
1216       },
1217     ];
1218   },
1221 PermissionUI.MIDIPermissionPrompt = MIDIPermissionPrompt;
1223 function StorageAccessPermissionPrompt(request) {
1224   this.request = request;
1227 StorageAccessPermissionPrompt.prototype = {
1228   __proto__: PermissionPromptForRequestPrototype,
1230   get usePermissionManager() {
1231     return false;
1232   },
1234   get type() {
1235     return "storage-access";
1236   },
1238   get permissionKey() {
1239     // Make sure this name is unique per each third-party tracker
1240     return "storage-access-" + this.principal.origin;
1241   },
1243   prettifyHostPort(hostport) {
1244     let [host, port] = hostport.split(":");
1245     host = IDNService.convertToDisplayIDN(host, {});
1246     if (port) {
1247       return `${host}:${port}`;
1248     }
1249     return host;
1250   },
1252   get popupOptions() {
1253     let learnMoreURL =
1254       Services.urlFormatter.formatURLPref("app.support.baseURL") +
1255       "third-party-cookies";
1256     let hostPort = this.prettifyHostPort(this.principal.hostPort);
1257     let hintText = gBrowserBundle.formatStringFromName(
1258       "storageAccess1.hintText",
1259       [hostPort]
1260     );
1261     return {
1262       learnMoreURL,
1263       displayURI: false,
1264       hintText,
1265       escAction: "secondarybuttoncommand",
1266     };
1267   },
1269   get notificationID() {
1270     return "storage-access";
1271   },
1273   get anchorID() {
1274     return "storage-access-notification-icon";
1275   },
1277   get message() {
1278     return gBrowserBundle.formatStringFromName("storageAccess4.message", [
1279       this.prettifyHostPort(this.principal.hostPort),
1280       this.prettifyHostPort(this.topLevelPrincipal.hostPort),
1281     ]);
1282   },
1284   get promptActions() {
1285     let self = this;
1287     return [
1288       {
1289         label: gBrowserBundle.GetStringFromName("storageAccess1.Allow.label"),
1290         accessKey: gBrowserBundle.GetStringFromName(
1291           "storageAccess1.Allow.accesskey"
1292         ),
1293         action: Ci.nsIPermissionManager.ALLOW_ACTION,
1294         callback(state) {
1295           self.allow({ "storage-access": "allow" });
1296         },
1297       },
1298       {
1299         label: gBrowserBundle.GetStringFromName(
1300           "storageAccess1.DontAllow.label"
1301         ),
1302         accessKey: gBrowserBundle.GetStringFromName(
1303           "storageAccess1.DontAllow.accesskey"
1304         ),
1305         action: Ci.nsIPermissionManager.DENY_ACTION,
1306         callback(state) {
1307           self.cancel();
1308         },
1309       },
1310     ];
1311   },
1313   get topLevelPrincipal() {
1314     return this.request.topLevelPrincipal;
1315   },
1318 PermissionUI.StorageAccessPermissionPrompt = StorageAccessPermissionPrompt;