summaryrefslogtreecommitdiffstats
path: root/browser/modules/PermissionUI.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/PermissionUI.sys.mjs')
-rw-r--r--browser/modules/PermissionUI.sys.mjs1429
1 files changed, 1429 insertions, 0 deletions
diff --git a/browser/modules/PermissionUI.sys.mjs b/browser/modules/PermissionUI.sys.mjs
new file mode 100644
index 0000000000..8fc173886b
--- /dev/null
+++ b/browser/modules/PermissionUI.sys.mjs
@@ -0,0 +1,1429 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * PermissionUI is responsible for exposing both a prototype
+ * PermissionPrompt that can be used by arbitrary browser
+ * components and add-ons, but also hosts the implementations of
+ * built-in permission prompts.
+ *
+ * If you're developing a feature that requires web content to ask
+ * for special permissions from the user, this module is for you.
+ *
+ * Suppose a system add-on wants to add a new prompt for a new request
+ * for getting more low-level access to the user's sound card, and the
+ * permission request is coming up from content by way of the
+ * nsContentPermissionHelper. The system add-on could then do the following:
+ *
+ * const { Integration } = ChromeUtils.importESModule(
+ * "resource://gre/modules/Integration.sys.mjs"
+ * );
+ * const { PermissionUI } = ChromeUtils.import(
+ * "resource:///modules/PermissionUI.jsm"
+ * );
+ *
+ * const SoundCardIntegration = base => {
+ * let soundCardObj = {
+ * createPermissionPrompt(type, request) {
+ * if (type != "sound-api") {
+ * return super.createPermissionPrompt(...arguments);
+ * }
+ *
+ * let permissionPrompt = {
+ * get permissionKey() {
+ * return "sound-permission";
+ * }
+ * // etc - see the documentation for PermissionPrompt for
+ * // a better idea of what things one can and should override.
+ * };
+ * Object.setPrototypeOf(
+ * permissionPrompt,
+ * PermissionUI.PermissionPromptForRequest
+ * );
+ * return permissionPrompt;
+ * },
+ * };
+ * Object.setPrototypeOf(soundCardObj, base);
+ * return soundCardObj;
+ * };
+ *
+ * // Add-on startup:
+ * Integration.contentPermission.register(SoundCardIntegration);
+ * // ...
+ * // Add-on shutdown:
+ * Integration.contentPermission.unregister(SoundCardIntegration);
+ *
+ * Note that PermissionPromptForRequest must be used as the
+ * prototype, since the prompt is wrapping an nsIContentPermissionRequest,
+ * and going through nsIContentPermissionPrompt.
+ *
+ * It is, however, possible to take advantage of PermissionPrompt without
+ * having to go through nsIContentPermissionPrompt or with a
+ * nsIContentPermissionRequest. The PermissionPrompt can be
+ * imported, subclassed, and have prompt() called directly, without
+ * the caller having called into createPermissionPrompt.
+ */
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "IDNService",
+ "@mozilla.org/network/idn-service;1",
+ "nsIIDNService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "ContentPrefService2",
+ "@mozilla.org/content-pref/service;1",
+ "nsIContentPrefService2"
+);
+XPCOMUtils.defineLazyGetter(lazy, "gBrowserBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+});
+
+import { SITEPERMS_ADDON_PROVIDER_PREF } from "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "sitePermsAddonsProviderEnabled",
+ SITEPERMS_ADDON_PROVIDER_PREF,
+ false
+);
+
+/**
+ * PermissionPrompt should be subclassed by callers that
+ * want to display prompts to the user. See each method and property
+ * below for guidance on what to override.
+ *
+ * Note that if you're creating a prompt for an
+ * nsIContentPermissionRequest, you'll want to subclass
+ * PermissionPromptForRequest instead.
+ */
+class PermissionPrompt {
+ /**
+ * Returns the associated <xul:browser> for the request. This should
+ * work for the e10s and non-e10s case.
+ *
+ * Subclasses must override this.
+ *
+ * @return {<xul:browser>}
+ */
+ get browser() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * Returns the nsIPrincipal associated with the request.
+ *
+ * Subclasses must override this.
+ *
+ * @return {nsIPrincipal}
+ */
+ get principal() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * Indicates the type of the permission request from content. This type might
+ * be different from the permission key used in the permissions database.
+ */
+ get type() {
+ return undefined;
+ }
+
+ /**
+ * If the nsIPermissionManager is being queried and written
+ * to for this permission request, set this to the key to be
+ * used. If this is undefined, no integration with temporary
+ * permissions infrastructure will be provided.
+ *
+ * Note that if a permission is set, in any follow-up
+ * prompting within the expiry window of that permission,
+ * the prompt will be skipped and the allow or deny choice
+ * will be selected automatically.
+ */
+ get permissionKey() {
+ return undefined;
+ }
+
+ /**
+ * If true, user permissions will be read from and written to.
+ * When this is false, we still provide integration with
+ * infrastructure such as temporary permissions. permissionKey should
+ * still return a valid name in those cases for that integration to work.
+ */
+ get usePermissionManager() {
+ return true;
+ }
+
+ /**
+ * Indicates what URI should be used as the scope when using temporary
+ * permissions. If undefined, it defaults to the browser.currentURI.
+ */
+ get temporaryPermissionURI() {
+ return undefined;
+ }
+
+ /**
+ * These are the options that will be passed to the PopupNotification when it
+ * is shown. See the documentation of `PopupNotifications_show` in
+ * PopupNotifications.sys.mjs for details.
+ *
+ * Note that prompt() will automatically set displayURI to
+ * be the URI of the requesting pricipal, unless the displayURI is exactly
+ * set to false.
+ */
+ get popupOptions() {
+ return {};
+ }
+
+ /**
+ * If true, automatically denied permission requests will
+ * spawn a "post-prompt" that allows the user to correct the
+ * automatic denial by giving permanent permission access to
+ * the site.
+ *
+ * Note that if this function returns true, the permissionKey
+ * and postPromptActions attributes must be implemented.
+ */
+ get postPromptEnabled() {
+ return false;
+ }
+
+ /**
+ * If true, the prompt will be cancelled automatically unless
+ * request.hasValidTransientUserGestureActivation is true.
+ */
+ get requiresUserInput() {
+ return false;
+ }
+
+ /**
+ * PopupNotification requires a unique ID to open the notification.
+ * You must return a unique ID string here, for which PopupNotification
+ * will then create a <xul:popupnotification> node with the ID
+ * "<notificationID>-notification".
+ *
+ * If there's a custom <xul:popupnotification> you're hoping to show,
+ * then you need to make sure its ID has the "-notification" suffix,
+ * and then return the prefix here.
+ *
+ * See PopupNotifications.sys.mjs for more details.
+ *
+ * @return {string}
+ * The unique ID that will be used to as the
+ * "<unique ID>-notification" ID for the <xul:popupnotification>
+ * to use or create.
+ */
+ get notificationID() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * The ID of the element to anchor the PopupNotification to.
+ *
+ * @return {string}
+ */
+ get anchorID() {
+ return "default-notification-icon";
+ }
+
+ /**
+ * The message to show to the user in the PopupNotification, see
+ * `PopupNotifications_show` in PopupNotifications.sys.mjs.
+ *
+ * Subclasses must override this.
+ *
+ * @return {string}
+ */
+ get message() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * Provides the preferred name to use in the permission popups,
+ * based on the principal URI (the URI.hostPort for any URI scheme
+ * besides the moz-extension one which should default to the
+ * extension name).
+ */
+ getPrincipalName(principal = this.principal) {
+ if (principal.addonPolicy) {
+ return principal.addonPolicy.name;
+ }
+
+ return principal.hostPort;
+ }
+
+ /**
+ * This will be called if the request is to be cancelled.
+ *
+ * Subclasses only need to override this if they provide a
+ * permissionKey.
+ */
+ cancel() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * This will be called if the request is to be allowed.
+ *
+ * Subclasses only need to override this if they provide a
+ * permissionKey.
+ */
+ allow() {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * The actions that will be displayed in the PopupNotification
+ * via a dropdown menu. The first item in this array will be
+ * the default selection. Each action is an Object with the
+ * following properties:
+ *
+ * label (string):
+ * The label that will be displayed for this choice.
+ * accessKey (string):
+ * The access key character that will be used for this choice.
+ * action (SitePermissions state)
+ * The action that will be associated with this choice.
+ * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
+ * scope (SitePermissions scope)
+ * The scope of the associated action (e.g. SitePermissions.SCOPE_PERSISTENT)
+ *
+ * callback (function, optional)
+ * A callback function that will fire if the user makes this choice, with
+ * a single parameter, state. State is an Object that contains the property
+ * checkboxChecked, which identifies whether the checkbox to remember this
+ * decision was checked.
+ */
+ get promptActions() {
+ return [];
+ }
+
+ /**
+ * The actions that will be displayed in the PopupNotification
+ * for post-prompt notifications via a dropdown menu.
+ * The first item in this array will be the default selection.
+ * Each action is an Object with the following properties:
+ *
+ * label (string):
+ * The label that will be displayed for this choice.
+ * accessKey (string):
+ * The access key character that will be used for this choice.
+ * action (SitePermissions state)
+ * The action that will be associated with this choice.
+ * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
+ * Note that the scope of this action will always be persistent.
+ *
+ * callback (function, optional)
+ * A callback function that will fire if the user makes this choice.
+ */
+ get postPromptActions() {
+ return null;
+ }
+
+ /**
+ * If the prompt will be shown to the user, this callback will
+ * be called just before. Subclasses may want to override this
+ * in order to, for example, bump a counter Telemetry probe for
+ * how often a particular permission request is seen.
+ *
+ * If this returns false, it cancels the process of showing the prompt. In
+ * that case, it is the responsibility of the onBeforeShow() implementation
+ * to ensure that allow() or cancel() are called on the object appropriately.
+ */
+ onBeforeShow() {
+ return true;
+ }
+
+ /**
+ * If the prompt was shown to the user, this callback will be called just
+ * after it's been shown.
+ */
+ onShown() {}
+
+ /**
+ * If the prompt was shown to the user, this callback will be called just
+ * after it's been hidden.
+ */
+ onAfterShow() {}
+
+ /**
+ * Will determine if a prompt should be shown to the user, and if so,
+ * will show it.
+ *
+ * If a permissionKey is defined prompt() might automatically
+ * allow or cancel itself based on the user's current
+ * permission settings without displaying the prompt.
+ *
+ * If the permission is not already set and the <xul:browser> that the request
+ * is associated with does not belong to a browser window with the
+ * PopupNotifications global set, the prompt request is ignored.
+ */
+ prompt() {
+ // We ignore requests from non-nsIStandardURLs
+ let requestingURI = this.principal.URI;
+ if (!(requestingURI instanceof Ci.nsIStandardURL)) {
+ return;
+ }
+
+ if (this.usePermissionManager && this.permissionKey) {
+ // If we're reading and setting permissions, then we need
+ // to check to see if we already have a permission setting
+ // for this particular principal.
+ let { state } = lazy.SitePermissions.getForPrincipal(
+ this.principal,
+ this.permissionKey,
+ this.browser,
+ this.temporaryPermissionURI
+ );
+
+ if (state == lazy.SitePermissions.BLOCK) {
+ // If this block was done based on a global user setting, we want to show
+ // a post prompt to give the user some more granular control without
+ // annoying them too much.
+ if (
+ this.postPromptEnabled &&
+ lazy.SitePermissions.getDefault(this.permissionKey) ==
+ lazy.SitePermissions.BLOCK
+ ) {
+ this.postPrompt();
+ }
+ this.cancel();
+ return;
+ }
+
+ if (
+ state == lazy.SitePermissions.ALLOW &&
+ !this.request.isRequestDelegatedToUnsafeThirdParty
+ ) {
+ this.allow();
+ return;
+ }
+ } else if (this.permissionKey) {
+ // If we're reading a permission which already has a temporary value,
+ // see if we can use the temporary value.
+ let { state } = lazy.SitePermissions.getForPrincipal(
+ null,
+ this.permissionKey,
+ this.browser,
+ this.temporaryPermissionURI
+ );
+
+ if (state == lazy.SitePermissions.BLOCK) {
+ this.cancel();
+ return;
+ }
+ }
+
+ if (
+ this.requiresUserInput &&
+ !this.request.hasValidTransientUserGestureActivation
+ ) {
+ if (this.postPromptEnabled) {
+ this.postPrompt();
+ }
+ this.cancel();
+ return;
+ }
+
+ let chromeWin = this.browser.ownerGlobal;
+ if (!chromeWin.PopupNotifications) {
+ this.cancel();
+ return;
+ }
+
+ // Transform the PermissionPrompt actions into PopupNotification actions.
+ let popupNotificationActions = [];
+ for (let promptAction of this.promptActions) {
+ let action = {
+ label: promptAction.label,
+ accessKey: promptAction.accessKey,
+ callback: state => {
+ if (promptAction.callback) {
+ promptAction.callback();
+ }
+
+ if (this.usePermissionManager && this.permissionKey) {
+ if (
+ (state && state.checkboxChecked && state.source != "esc-press") ||
+ promptAction.scope == lazy.SitePermissions.SCOPE_PERSISTENT
+ ) {
+ // Permanently store permission.
+ let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
+ // Only remember permission for session if in PB mode.
+ if (lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
+ scope = lazy.SitePermissions.SCOPE_SESSION;
+ }
+ lazy.SitePermissions.setForPrincipal(
+ this.principal,
+ this.permissionKey,
+ promptAction.action,
+ scope
+ );
+ } else if (promptAction.action == lazy.SitePermissions.BLOCK) {
+ // Temporarily store BLOCK permissions only
+ // SitePermissions does not consider subframes when storing temporary
+ // permissions on a tab, thus storing ALLOW could be exploited.
+ lazy.SitePermissions.setForPrincipal(
+ this.principal,
+ this.permissionKey,
+ promptAction.action,
+ lazy.SitePermissions.SCOPE_TEMPORARY,
+ this.browser
+ );
+ }
+
+ // Grant permission if action is ALLOW.
+ if (promptAction.action == lazy.SitePermissions.ALLOW) {
+ this.allow();
+ } else {
+ this.cancel();
+ }
+ } else if (this.permissionKey) {
+ // TODO: Add support for permitTemporaryAllow
+ if (promptAction.action == lazy.SitePermissions.BLOCK) {
+ // Temporarily store BLOCK permissions.
+ // We don't consider subframes when storing temporary
+ // permissions on a tab, thus storing ALLOW could be exploited.
+ lazy.SitePermissions.setForPrincipal(
+ null,
+ this.permissionKey,
+ promptAction.action,
+ lazy.SitePermissions.SCOPE_TEMPORARY,
+ this.browser
+ );
+ }
+ }
+ },
+ };
+ if (promptAction.dismiss) {
+ action.dismiss = promptAction.dismiss;
+ }
+
+ popupNotificationActions.push(action);
+ }
+
+ this.#showNotification(popupNotificationActions);
+ }
+
+ postPrompt() {
+ let browser = this.browser;
+ let principal = this.principal;
+ let chromeWin = browser.ownerGlobal;
+ if (!chromeWin.PopupNotifications) {
+ return;
+ }
+
+ if (!this.permissionKey) {
+ throw new Error("permissionKey is required to show a post-prompt");
+ }
+
+ if (!this.postPromptActions) {
+ throw new Error("postPromptActions are required to show a post-prompt");
+ }
+
+ // Transform the PermissionPrompt actions into PopupNotification actions.
+ let popupNotificationActions = [];
+ for (let promptAction of this.postPromptActions) {
+ let action = {
+ label: promptAction.label,
+ accessKey: promptAction.accessKey,
+ callback: state => {
+ if (promptAction.callback) {
+ promptAction.callback();
+ }
+
+ // Post-prompt permissions are stored permanently by default.
+ // Since we can not reply to the original permission request anymore,
+ // the page will need to listen for permission changes which are triggered
+ // by permanent entries in the permission manager.
+ let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
+ // Only remember permission for session if in PB mode.
+ if (lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ scope = lazy.SitePermissions.SCOPE_SESSION;
+ }
+ lazy.SitePermissions.setForPrincipal(
+ principal,
+ this.permissionKey,
+ promptAction.action,
+ scope
+ );
+ },
+ };
+ popupNotificationActions.push(action);
+ }
+
+ // Post-prompt animation
+ if (!chromeWin.gReduceMotion) {
+ let anchor = chromeWin.document.getElementById(this.anchorID);
+ // Only show the animation on the first request, not after e.g. tab switching.
+ anchor.addEventListener(
+ "animationend",
+ () => anchor.removeAttribute("animate"),
+ { once: true }
+ );
+ anchor.setAttribute("animate", "true");
+ }
+
+ this.#showNotification(popupNotificationActions, true);
+ }
+
+ #showNotification(actions, postPrompt = false) {
+ let chromeWin = this.browser.ownerGlobal;
+ let mainAction = actions.length ? actions[0] : null;
+ let secondaryActions = actions.splice(1);
+
+ let options = this.popupOptions;
+
+ if (!options.hasOwnProperty("displayURI") || options.displayURI) {
+ options.displayURI = this.principal.URI;
+ }
+
+ if (!postPrompt) {
+ // Permission prompts are always persistent; the close button is controlled by a pref.
+ options.persistent = true;
+ options.hideClose = true;
+ }
+
+ options.eventCallback = (topic, nextRemovalReason, isCancel) => {
+ // When the docshell of the browser is aboout to be swapped to another one,
+ // the "swapping" event is called. Returning true causes the notification
+ // to be moved to the new browser.
+ if (topic == "swapping") {
+ return true;
+ }
+ // The prompt has been shown, notify the PermissionUI.
+ // onShown() is currently not called for post-prompts,
+ // because there is no prompt that would make use of this.
+ // You can remove this restriction if you need it, but be
+ // mindful of other consumers.
+ if (topic == "shown" && !postPrompt) {
+ this.onShown();
+ }
+ // The prompt has been removed, notify the PermissionUI.
+ // onAfterShow() is currently not called for post-prompts,
+ // because there is no prompt that would make use of this.
+ // You can remove this restriction if you need it, but be
+ // mindful of other consumers.
+ if (topic == "removed" && !postPrompt) {
+ if (isCancel) {
+ this.cancel();
+ }
+ this.onAfterShow();
+ }
+ return false;
+ };
+
+ // Post-prompts show up as dismissed.
+ options.dismissed = postPrompt;
+
+ // onBeforeShow() is currently not called for post-prompts,
+ // because there is no prompt that would make use of this.
+ // You can remove this restriction if you need it, but be
+ // mindful of other consumers.
+ if (postPrompt || this.onBeforeShow() !== false) {
+ chromeWin.PopupNotifications.show(
+ this.browser,
+ this.notificationID,
+ this.message,
+ this.anchorID,
+ mainAction,
+ secondaryActions,
+ options
+ );
+ }
+ }
+}
+
+/**
+ * A subclass of PermissionPrompt that assumes
+ * that this.request is an nsIContentPermissionRequest
+ * and fills in some of the required properties on the
+ * PermissionPrompt. For callers that are wrapping an
+ * nsIContentPermissionRequest, this should be subclassed
+ * rather than PermissionPrompt.
+ */
+class PermissionPromptForRequest extends PermissionPrompt {
+ get browser() {
+ // In the e10s-case, the <xul:browser> will be at request.element.
+ // In the single-process case, we have to use some XPCOM incantations
+ // to resolve to the <xul:browser>.
+ if (this.request.element) {
+ return this.request.element;
+ }
+ return this.request.window.docShell.chromeEventHandler;
+ }
+
+ get principal() {
+ let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest);
+ return request.getDelegatePrincipal(this.type);
+ }
+
+ cancel() {
+ this.request.cancel();
+ }
+
+ allow(choices) {
+ this.request.allow(choices);
+ }
+}
+
+/**
+ * A subclass of PermissionPromptForRequest that prompts
+ * for a Synthetic SitePermsAddon addon type and starts a synthetic
+ * addon install flow.
+ */
+class SitePermsAddonInstallRequest extends PermissionPromptForRequest {
+ prompt() {
+ // fallback to regular permission prompt for localhost,
+ // or when the SitePermsAddonProvider is not enabled.
+ if (this.principal.isLoopbackHost || !lazy.sitePermsAddonsProviderEnabled) {
+ super.prompt();
+ return;
+ }
+
+ // Otherwise, we'll use the addon install flow.
+ lazy.AddonManager.installSitePermsAddonFromWebpage(
+ this.browser,
+ this.principal,
+ this.permName
+ ).then(
+ () => {
+ this.allow();
+ },
+ err => {
+ this.cancel();
+
+ // Print an error message in the console to give more information to the developer.
+ let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
+ let errorMessage =
+ this.getInstallErrorMessage(err) ||
+ `${this.permName} access was rejected: ${err.message}`;
+
+ let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
+ scriptError.initWithWindowID(
+ errorMessage,
+ null,
+ null,
+ 0,
+ 0,
+ 0,
+ "content javascript",
+ this.browser.browsingContext.currentWindowGlobal.innerWindowId
+ );
+ Services.console.logMessage(scriptError);
+ }
+ );
+ }
+
+ /**
+ * Returns an error message that will be printed to the console given a passed Component.Exception.
+ * This should be overriden by children classes.
+ *
+ * @param {Components.Exception} err
+ * @returns {String} The error message
+ */
+ getInstallErrorMessage(err) {
+ return null;
+ }
+}
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the GeoLocation API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ * The request for a permission from content.
+ */
+class GeolocationPermissionPrompt extends PermissionPromptForRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+ }
+
+ get type() {
+ return "geo";
+ }
+
+ get permissionKey() {
+ return "geo";
+ }
+
+ get popupOptions() {
+ let pref = "browser.geolocation.warning.infoURL";
+ let options = {
+ learnMoreURL: Services.urlFormatter.formatURLPref(pref),
+ displayURI: false,
+ name: this.getPrincipalName(),
+ };
+
+ // Don't offer "always remember" action in PB mode
+ options.checkbox = {
+ show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
+ this.browser.ownerGlobal
+ ),
+ };
+
+ if (this.request.isRequestDelegatedToUnsafeThirdParty) {
+ // Second name should be the third party origin
+ options.secondName = this.getPrincipalName(this.request.principal);
+ options.checkbox = { show: false };
+ }
+
+ if (options.checkbox.show) {
+ options.checkbox.label = lazy.gBrowserBundle.GetStringFromName(
+ "geolocation.remember"
+ );
+ }
+
+ return options;
+ }
+
+ get notificationID() {
+ return "geolocation";
+ }
+
+ get anchorID() {
+ return "geo-notification-icon";
+ }
+
+ get message() {
+ if (this.principal.schemeIs("file")) {
+ return lazy.gBrowserBundle.GetStringFromName(
+ "geolocation.shareWithFile4"
+ );
+ }
+
+ if (this.request.isRequestDelegatedToUnsafeThirdParty) {
+ return lazy.gBrowserBundle.formatStringFromName(
+ "geolocation.shareWithSiteUnsafeDelegation2",
+ ["<>", "{}"]
+ );
+ }
+
+ return lazy.gBrowserBundle.formatStringFromName(
+ "geolocation.shareWithSite4",
+ ["<>"]
+ );
+ }
+
+ get promptActions() {
+ return [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("geolocation.allow"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "geolocation.allow.accesskey"
+ ),
+ action: lazy.SitePermissions.ALLOW,
+ },
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("geolocation.block"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "geolocation.block.accesskey"
+ ),
+ action: lazy.SitePermissions.BLOCK,
+ },
+ ];
+ }
+
+ #updateGeoSharing(state) {
+ let gBrowser = this.browser.ownerGlobal.gBrowser;
+ if (gBrowser == null) {
+ return;
+ }
+ gBrowser.updateBrowserSharing(this.browser, { geo: state });
+
+ // Update last access timestamp
+ let host;
+ try {
+ host = this.browser.currentURI.host;
+ } catch (e) {
+ return;
+ }
+ if (host == null || host == "") {
+ return;
+ }
+ lazy.ContentPrefService2.set(
+ this.browser.currentURI.host,
+ "permissions.geoLocation.lastAccess",
+ new Date().toString(),
+ this.browser.loadContext
+ );
+ }
+
+ allow(...args) {
+ this.#updateGeoSharing(true);
+ super.allow(...args);
+ }
+
+ cancel(...args) {
+ this.#updateGeoSharing(false);
+ super.cancel(...args);
+ }
+}
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the WebXR API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ * The request for a permission from content.
+ */
+class XRPermissionPrompt extends PermissionPromptForRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+ }
+
+ get type() {
+ return "xr";
+ }
+
+ get permissionKey() {
+ return "xr";
+ }
+
+ get popupOptions() {
+ let pref = "browser.xr.warning.infoURL";
+ let options = {
+ learnMoreURL: Services.urlFormatter.formatURLPref(pref),
+ displayURI: false,
+ name: this.getPrincipalName(),
+ };
+
+ // Don't offer "always remember" action in PB mode
+ options.checkbox = {
+ show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
+ this.browser.ownerGlobal
+ ),
+ };
+
+ if (options.checkbox.show) {
+ options.checkbox.label =
+ lazy.gBrowserBundle.GetStringFromName("xr.remember");
+ }
+
+ return options;
+ }
+
+ get notificationID() {
+ return "xr";
+ }
+
+ get anchorID() {
+ return "xr-notification-icon";
+ }
+
+ get message() {
+ if (this.principal.schemeIs("file")) {
+ return lazy.gBrowserBundle.GetStringFromName("xr.shareWithFile4");
+ }
+
+ return lazy.gBrowserBundle.formatStringFromName("xr.shareWithSite4", [
+ "<>",
+ ]);
+ }
+
+ get promptActions() {
+ return [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("xr.allow2"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName("xr.allow2.accesskey"),
+ action: lazy.SitePermissions.ALLOW,
+ },
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("xr.block"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName("xr.block.accesskey"),
+ action: lazy.SitePermissions.BLOCK,
+ },
+ ];
+ }
+
+ #updateXRSharing(state) {
+ let gBrowser = this.browser.ownerGlobal.gBrowser;
+ if (gBrowser == null) {
+ return;
+ }
+ gBrowser.updateBrowserSharing(this.browser, { xr: state });
+
+ let devicePermOrigins = this.browser.getDevicePermissionOrigins("xr");
+ if (!state) {
+ devicePermOrigins.delete(this.principal.origin);
+ return;
+ }
+ devicePermOrigins.add(this.principal.origin);
+ }
+
+ allow(...args) {
+ this.#updateXRSharing(true);
+ super.allow(...args);
+ }
+
+ cancel(...args) {
+ this.#updateXRSharing(false);
+ super.cancel(...args);
+ }
+}
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the Desktop Notification API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ * The request for a permission from content.
+ * @return {PermissionPrompt} (see documentation in header)
+ */
+class DesktopNotificationPermissionPrompt extends PermissionPromptForRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "requiresUserInput",
+ "dom.webnotifications.requireuserinteraction"
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "postPromptEnabled",
+ "permissions.desktop-notification.postPrompt.enabled"
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "notNowEnabled",
+ "permissions.desktop-notification.notNow.enabled"
+ );
+ }
+
+ get type() {
+ return "desktop-notification";
+ }
+
+ get permissionKey() {
+ return "desktop-notification";
+ }
+
+ get popupOptions() {
+ let learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
+
+ return {
+ learnMoreURL,
+ displayURI: false,
+ name: this.getPrincipalName(),
+ };
+ }
+
+ get notificationID() {
+ return "web-notifications";
+ }
+
+ get anchorID() {
+ return "web-notifications-notification-icon";
+ }
+
+ get message() {
+ return lazy.gBrowserBundle.formatStringFromName(
+ "webNotifications.receiveFromSite3",
+ ["<>"]
+ );
+ }
+
+ get promptActions() {
+ let actions = [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.allow2.accesskey"
+ ),
+ action: lazy.SitePermissions.ALLOW,
+ scope: lazy.SitePermissions.SCOPE_PERSISTENT,
+ },
+ ];
+ if (this.notNowEnabled) {
+ actions.push({
+ label: lazy.gBrowserBundle.GetStringFromName("webNotifications.notNow"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.notNow.accesskey"
+ ),
+ action: lazy.SitePermissions.BLOCK,
+ });
+ }
+
+ let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
+ this.browser
+ );
+ actions.push({
+ label: isBrowserPrivate
+ ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
+ : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
+ accessKey: isBrowserPrivate
+ ? lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.block.accesskey"
+ )
+ : lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.alwaysBlock.accesskey"
+ ),
+ action: lazy.SitePermissions.BLOCK,
+ scope: isBrowserPrivate
+ ? lazy.SitePermissions.SCOPE_SESSION
+ : lazy.SitePermissions.SCOPE_PERSISTENT,
+ });
+ return actions;
+ }
+
+ get postPromptActions() {
+ let actions = [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.allow2.accesskey"
+ ),
+ action: lazy.SitePermissions.ALLOW,
+ },
+ ];
+
+ let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
+ this.browser
+ );
+ actions.push({
+ label: isBrowserPrivate
+ ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
+ : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
+ accessKey: isBrowserPrivate
+ ? lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.block.accesskey"
+ )
+ : lazy.gBrowserBundle.GetStringFromName(
+ "webNotifications.alwaysBlock.accesskey"
+ ),
+ action: lazy.SitePermissions.BLOCK,
+ });
+ return actions;
+ }
+}
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the persistent-storage API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ * The request for a permission from content.
+ */
+class PersistentStoragePermissionPrompt extends PermissionPromptForRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+ }
+
+ get type() {
+ return "persistent-storage";
+ }
+
+ get permissionKey() {
+ return "persistent-storage";
+ }
+
+ get popupOptions() {
+ let learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "storage-permissions";
+ return {
+ learnMoreURL,
+ displayURI: false,
+ name: this.getPrincipalName(),
+ };
+ }
+
+ get notificationID() {
+ return "persistent-storage";
+ }
+
+ get anchorID() {
+ return "persistent-storage-notification-icon";
+ }
+
+ get message() {
+ return lazy.gBrowserBundle.formatStringFromName(
+ "persistentStorage.allowWithSite2",
+ ["<>"]
+ );
+ }
+
+ get promptActions() {
+ return [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("persistentStorage.allow"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "persistentStorage.allow.accesskey"
+ ),
+ action: Ci.nsIPermissionManager.ALLOW_ACTION,
+ scope: lazy.SitePermissions.SCOPE_PERSISTENT,
+ },
+ {
+ label: lazy.gBrowserBundle.GetStringFromName(
+ "persistentStorage.block.label"
+ ),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "persistentStorage.block.accesskey"
+ ),
+ action: lazy.SitePermissions.BLOCK,
+ },
+ ];
+ }
+}
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the WebMIDI API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ * The request for a permission from content.
+ */
+class MIDIPermissionPrompt extends SitePermsAddonInstallRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+ let types = request.types.QueryInterface(Ci.nsIArray);
+ let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+ this.isSysexPerm =
+ !!perm.options.length &&
+ perm.options.queryElementAt(0, Ci.nsISupportsString) == "sysex";
+ this.permName = "midi";
+ if (this.isSysexPerm) {
+ this.permName = "midi-sysex";
+ }
+ }
+
+ get type() {
+ return "midi";
+ }
+
+ get permissionKey() {
+ return this.permName;
+ }
+
+ get popupOptions() {
+ // TODO (bug 1433235) We need a security/permissions explanation URL for this
+ let options = {
+ displayURI: false,
+ name: this.getPrincipalName(),
+ };
+
+ // Don't offer "always remember" action in PB mode
+ options.checkbox = {
+ show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
+ this.browser.ownerGlobal
+ ),
+ };
+
+ if (options.checkbox.show) {
+ options.checkbox.label =
+ lazy.gBrowserBundle.GetStringFromName("midi.remember");
+ }
+
+ return options;
+ }
+
+ get notificationID() {
+ return "midi";
+ }
+
+ get anchorID() {
+ return "midi-notification-icon";
+ }
+
+ get message() {
+ let message;
+ if (this.principal.schemeIs("file")) {
+ if (this.isSysexPerm) {
+ message = lazy.gBrowserBundle.GetStringFromName(
+ "midi.shareSysexWithFile"
+ );
+ } else {
+ message = lazy.gBrowserBundle.GetStringFromName("midi.shareWithFile");
+ }
+ } else if (this.isSysexPerm) {
+ message = lazy.gBrowserBundle.formatStringFromName(
+ "midi.shareSysexWithSite",
+ ["<>"]
+ );
+ } else {
+ message = lazy.gBrowserBundle.formatStringFromName("midi.shareWithSite", [
+ "<>",
+ ]);
+ }
+ return message;
+ }
+
+ get promptActions() {
+ return [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("midi.allow.label"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "midi.allow.accesskey"
+ ),
+ action: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ label: lazy.gBrowserBundle.GetStringFromName("midi.block.label"),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "midi.block.accesskey"
+ ),
+ action: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ ];
+ }
+
+ /**
+ * @override
+ * @param {Components.Exception} err
+ * @returns {String}
+ */
+ getInstallErrorMessage(err) {
+ return `WebMIDI access request was denied: ❝${err.message}❞. See https://developer.mozilla.org/docs/Web/API/Navigator/requestMIDIAccess for more information`;
+ }
+}
+
+class StorageAccessPermissionPrompt extends PermissionPromptForRequest {
+ constructor(request) {
+ super();
+ this.request = request;
+ this.siteOption = null;
+
+ let types = this.request.types.QueryInterface(Ci.nsIArray);
+ let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+ let options = perm.options.QueryInterface(Ci.nsIArray);
+ // If we have an option, we are in a call from requestStorageAccessUnderSite
+ // which means that the embedding principal is not the current top-level.
+ // Instead we have to grab the Site string out of the option and use that
+ // in the UI.
+ if (options.length) {
+ this.siteOption = options.queryElementAt(0, Ci.nsISupportsString).data;
+ }
+ }
+
+ get usePermissionManager() {
+ return false;
+ }
+
+ get type() {
+ return "storage-access";
+ }
+
+ get permissionKey() {
+ // Make sure this name is unique per each third-party tracker
+ return `3rdPartyStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.origin}`;
+ }
+
+ get temporaryPermissionURI() {
+ if (this.siteOption) {
+ return Services.io.newURI(this.siteOption);
+ }
+ return undefined;
+ }
+
+ prettifyHostPort(hostport) {
+ let [host, port] = hostport.split(":");
+ host = lazy.IDNService.convertToDisplayIDN(host, {});
+ if (port) {
+ return `${host}:${port}`;
+ }
+ return host;
+ }
+
+ get popupOptions() {
+ let learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "third-party-cookies";
+ let hostPort = this.prettifyHostPort(this.principal.hostPort);
+ let hintText = lazy.gBrowserBundle.formatStringFromName(
+ "storageAccess1.hintText",
+ [hostPort]
+ );
+ return {
+ learnMoreURL,
+ displayURI: false,
+ hintText,
+ escAction: "secondarybuttoncommand",
+ };
+ }
+
+ get notificationID() {
+ return "storage-access";
+ }
+
+ get anchorID() {
+ return "storage-access-notification-icon";
+ }
+
+ get message() {
+ let embeddingHost = this.topLevelPrincipal.host;
+
+ if (this.siteOption) {
+ embeddingHost = this.siteOption.split("://").at(-1);
+ }
+
+ return lazy.gBrowserBundle.formatStringFromName("storageAccess4.message", [
+ this.prettifyHostPort(this.principal.hostPort),
+ this.prettifyHostPort(embeddingHost),
+ ]);
+ }
+
+ get promptActions() {
+ let self = this;
+
+ return [
+ {
+ label: lazy.gBrowserBundle.GetStringFromName(
+ "storageAccess1.Allow.label"
+ ),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "storageAccess1.Allow.accesskey"
+ ),
+ action: Ci.nsIPermissionManager.ALLOW_ACTION,
+ callback(state) {
+ self.allow({ "storage-access": "allow" });
+ },
+ },
+ {
+ label: lazy.gBrowserBundle.GetStringFromName(
+ "storageAccess1.DontAllow.label"
+ ),
+ accessKey: lazy.gBrowserBundle.GetStringFromName(
+ "storageAccess1.DontAllow.accesskey"
+ ),
+ action: Ci.nsIPermissionManager.DENY_ACTION,
+ callback(state) {
+ self.cancel();
+ },
+ },
+ ];
+ }
+
+ get topLevelPrincipal() {
+ return this.request.topLevelPrincipal;
+ }
+}
+
+export const PermissionUI = {
+ PermissionPromptForRequest,
+ GeolocationPermissionPrompt,
+ XRPermissionPrompt,
+ DesktopNotificationPermissionPrompt,
+ PersistentStoragePermissionPrompt,
+ MIDIPermissionPrompt,
+ StorageAccessPermissionPrompt,
+};