tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PermissionUI.sys.mjs (48716B)


      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/. */
      4 
      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";
     68 
     69 const lazy = {};
     70 
     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  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     76  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     77 });
     78 
     79 XPCOMUtils.defineLazyServiceGetter(
     80  lazy,
     81  "IDNService",
     82  "@mozilla.org/network/idn-service;1",
     83  Ci.nsIIDNService
     84 );
     85 XPCOMUtils.defineLazyServiceGetter(
     86  lazy,
     87  "ContentPrefService2",
     88  "@mozilla.org/content-pref/service;1",
     89  Ci.nsIContentPrefService2
     90 );
     91 
     92 ChromeUtils.defineLazyGetter(lazy, "gBrandBundle", function () {
     93  return Services.strings.createBundle(
     94    "chrome://branding/locale/brand.properties"
     95  );
     96 });
     97 
     98 ChromeUtils.defineLazyGetter(lazy, "gBrowserBundle", function () {
     99  return Services.strings.createBundle(
    100    "chrome://browser/locale/browser.properties"
    101  );
    102 });
    103 
    104 ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () {
    105  return new Localization(["browser/permissions.ftl"], true /* aSync */);
    106 });
    107 
    108 import { SITEPERMS_ADDON_PROVIDER_PREF } from "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs";
    109 
    110 XPCOMUtils.defineLazyPreferenceGetter(
    111  lazy,
    112  "sitePermsAddonsProviderEnabled",
    113  SITEPERMS_ADDON_PROVIDER_PREF,
    114  false
    115 );
    116 
    117 XPCOMUtils.defineLazyPreferenceGetter(
    118  lazy,
    119  "lnaPromptTimeoutMs",
    120  "network.lna.prompt.timeout",
    121  300000
    122 );
    123 
    124 /**
    125 * PermissionPrompt should be subclassed by callers that
    126 * want to display prompts to the user. See each method and property
    127 * below for guidance on what to override.
    128 *
    129 * Note that if you're creating a prompt for an
    130 * nsIContentPermissionRequest, you'll want to subclass
    131 * PermissionPromptForRequest instead.
    132 */
    133 class PermissionPrompt {
    134  /**
    135   * Returns the associated <xul:browser> for the request. This should
    136   * work for the e10s and non-e10s case.
    137   *
    138   * Subclasses must override this.
    139   *
    140   * @return {<xul:browser>}
    141   */
    142  get browser() {
    143    throw new Error("Not implemented.");
    144  }
    145 
    146  /**
    147   * Returns the nsIPrincipal associated with the request.
    148   *
    149   * Subclasses must override this.
    150   *
    151   * @return {nsIPrincipal}
    152   */
    153  get principal() {
    154    throw new Error("Not implemented.");
    155  }
    156 
    157  /**
    158   * Indicates the type of the permission request from content. This type might
    159   * be different from the permission key used in the permissions database.
    160   */
    161  get type() {
    162    return undefined;
    163  }
    164 
    165  /**
    166   * If the nsIPermissionManager is being queried and written
    167   * to for this permission request, set this to the key to be
    168   * used. If this is undefined, no integration with temporary
    169   * permissions infrastructure will be provided.
    170   *
    171   * Note that if a permission is set, in any follow-up
    172   * prompting within the expiry window of that permission,
    173   * the prompt will be skipped and the allow or deny choice
    174   * will be selected automatically.
    175   */
    176  get permissionKey() {
    177    return undefined;
    178  }
    179 
    180  /**
    181   * If true, user permissions will be read from and written to.
    182   * When this is false, we still provide integration with
    183   * infrastructure such as temporary permissions. permissionKey should
    184   * still return a valid name in those cases for that integration to work.
    185   */
    186  get usePermissionManager() {
    187    return true;
    188  }
    189 
    190  /**
    191   * Indicates what URI should be used as the scope when using temporary
    192   * permissions. If undefined, it defaults to the browser.currentURI.
    193   */
    194  get temporaryPermissionURI() {
    195    return undefined;
    196  }
    197 
    198  /**
    199   * These are the options that will be passed to the PopupNotification when it
    200   * is shown. See the documentation of `PopupNotifications_show` in
    201   * PopupNotifications.sys.mjs for details.
    202   *
    203   * Note that prompt() will automatically set displayURI to
    204   * be the URI of the requesting pricipal, unless the displayURI is exactly
    205   * set to false.
    206   */
    207  get popupOptions() {
    208    return {};
    209  }
    210 
    211  /**
    212   * If true, automatically denied permission requests will
    213   * spawn a "post-prompt" that allows the user to correct the
    214   * automatic denial by giving permanent permission access to
    215   * the site.
    216   *
    217   * Note that if this function returns true, the permissionKey
    218   * and postPromptActions attributes must be implemented.
    219   */
    220  get postPromptEnabled() {
    221    return false;
    222  }
    223 
    224  /**
    225   * If true, the prompt will be cancelled automatically unless
    226   * request.hasValidTransientUserGestureActivation is true.
    227   */
    228  get requiresUserInput() {
    229    return false;
    230  }
    231 
    232  /**
    233   * PopupNotification requires a unique ID to open the notification.
    234   * You must return a unique ID string here, for which PopupNotification
    235   * will then create a <xul:popupnotification> node with the ID
    236   * "<notificationID>-notification".
    237   *
    238   * If there's a custom <xul:popupnotification> you're hoping to show,
    239   * then you need to make sure its ID has the "-notification" suffix,
    240   * and then return the prefix here.
    241   *
    242   * See PopupNotifications.sys.mjs for more details.
    243   *
    244   * @return {string}
    245   *         The unique ID that will be used to as the
    246   *         "<unique ID>-notification" ID for the <xul:popupnotification>
    247   *         to use or create.
    248   */
    249  get notificationID() {
    250    throw new Error("Not implemented.");
    251  }
    252 
    253  /**
    254   * The ID of the element to anchor the PopupNotification to.
    255   *
    256   * @return {string}
    257   */
    258  get anchorID() {
    259    return "default-notification-icon";
    260  }
    261 
    262  /**
    263   * The message to show to the user in the PopupNotification, see
    264   * `PopupNotifications_show` in PopupNotifications.sys.mjs.
    265   *
    266   * Subclasses must override this.
    267   *
    268   * @return {string}
    269   */
    270  get message() {
    271    throw new Error("Not implemented.");
    272  }
    273 
    274  /**
    275   * The hint text to show to the user in the PopupNotification, see
    276   * `PopupNotifications_show` in PopupNotifications.sys.mjs.
    277   * By default, no hint is shown.
    278   *
    279   * @return {string}
    280   */
    281  get hintText() {
    282    return undefined;
    283  }
    284 
    285  /**
    286   * Provides the preferred name to use in the permission popups,
    287   * based on the principal URI (the URI.hostPort for any URI scheme
    288   * besides the moz-extension one which should default to the
    289   * extension name).
    290   */
    291  getPrincipalName(principal = this.principal) {
    292    if (principal.addonPolicy) {
    293      return principal.addonPolicy.name;
    294    }
    295 
    296    return principal.hostPort;
    297  }
    298 
    299  /**
    300   * This will be called if the request is to be cancelled.
    301   *
    302   * Subclasses only need to override this if they provide a
    303   * permissionKey.
    304   */
    305  cancel() {
    306    throw new Error("Not implemented.");
    307  }
    308 
    309  /**
    310   * This will be called if the request is to be allowed.
    311   *
    312   * Subclasses only need to override this if they provide a
    313   * permissionKey.
    314   */
    315  allow() {
    316    throw new Error("Not implemented.");
    317  }
    318 
    319  /**
    320   * The actions that will be displayed in the PopupNotification
    321   * via a dropdown menu. The first item in this array will be
    322   * the default selection. Each action is an Object with the
    323   * following properties:
    324   *
    325   *  label (string):
    326   *    The label that will be displayed for this choice.
    327   *  accessKey (string):
    328   *    The access key character that will be used for this choice.
    329   *  action (SitePermissions state)
    330   *    The action that will be associated with this choice.
    331   *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
    332   *  scope (SitePermissions scope)
    333   *    The scope of the associated action (e.g. SitePermissions.SCOPE_PERSISTENT)
    334   *
    335   *  callback (function, optional)
    336   *    A callback function that will fire if the user makes this choice, with
    337   *    a single parameter, state. State is an Object that contains the property
    338   *    checkboxChecked, which identifies whether the checkbox to remember this
    339   *    decision was checked.
    340   */
    341  get promptActions() {
    342    return [];
    343  }
    344 
    345  /**
    346   * The actions that will be displayed in the PopupNotification
    347   * for post-prompt notifications via a dropdown menu.
    348   * The first item in this array will be the default selection.
    349   * Each action is an Object with the following properties:
    350   *
    351   *  label (string):
    352   *    The label that will be displayed for this choice.
    353   *  accessKey (string):
    354   *    The access key character that will be used for this choice.
    355   *  action (SitePermissions state)
    356   *    The action that will be associated with this choice.
    357   *    This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
    358   *    Note that the scope of this action will always be persistent.
    359   *
    360   *  callback (function, optional)
    361   *    A callback function that will fire if the user makes this choice.
    362   */
    363  get postPromptActions() {
    364    return null;
    365  }
    366 
    367  /**
    368   * If the prompt will be shown to the user, this callback will
    369   * be called just before. Subclasses may want to override this
    370   * in order to, for example, bump a counter Telemetry probe for
    371   * how often a particular permission request is seen.
    372   *
    373   * If this returns false, it cancels the process of showing the prompt.  In
    374   * that case, it is the responsibility of the onBeforeShow() implementation
    375   * to ensure that allow() or cancel() are called on the object appropriately.
    376   */
    377  onBeforeShow() {
    378    return true;
    379  }
    380 
    381  /**
    382   * If the prompt was shown to the user, this callback will be called just
    383   * after it's been shown.
    384   */
    385  onShown() {}
    386 
    387  /**
    388   * If the prompt was shown to the user, this callback will be called just
    389   * after it's been hidden.
    390   */
    391  onAfterShow() {}
    392 
    393  /**
    394   * Will determine if a prompt should be shown to the user, and if so,
    395   * will show it.
    396   *
    397   * If a permissionKey is defined prompt() might automatically
    398   * allow or cancel itself based on the user's current
    399   * permission settings without displaying the prompt.
    400   *
    401   * If the permission is not already set and the <xul:browser> that the request
    402   * is associated with does not belong to a browser window with the
    403   * PopupNotifications global set, the prompt request is ignored.
    404   */
    405  prompt() {
    406    // We ignore requests from non-nsIStandardURLs
    407    let requestingURI = this.principal.URI;
    408    if (!(requestingURI instanceof Ci.nsIStandardURL)) {
    409      return;
    410    }
    411 
    412    if (this.usePermissionManager && this.permissionKey) {
    413      // If we're reading and setting permissions, then we need
    414      // to check to see if we already have a permission setting
    415      // for this particular principal.
    416      let { state } = lazy.SitePermissions.getForPrincipal(
    417        this.principal,
    418        this.permissionKey,
    419        this.browser,
    420        this.temporaryPermissionURI
    421      );
    422 
    423      if (state == lazy.SitePermissions.BLOCK) {
    424        this.cancel();
    425        return;
    426      }
    427 
    428      if (
    429        state == lazy.SitePermissions.ALLOW &&
    430        !this.request.isRequestDelegatedToUnsafeThirdParty
    431      ) {
    432        this.allow();
    433        return;
    434      }
    435    } else if (this.permissionKey) {
    436      // If we're reading a permission which already has a temporary value,
    437      // see if we can use the temporary value.
    438      let { state } = lazy.SitePermissions.getForPrincipal(
    439        null,
    440        this.permissionKey,
    441        this.browser,
    442        this.temporaryPermissionURI
    443      );
    444 
    445      if (state == lazy.SitePermissions.BLOCK) {
    446        this.cancel();
    447        return;
    448      }
    449    }
    450 
    451    if (
    452      this.requiresUserInput &&
    453      !this.request.hasValidTransientUserGestureActivation
    454    ) {
    455      if (this.postPromptEnabled) {
    456        this.postPrompt();
    457      }
    458      this.cancel();
    459      return;
    460    }
    461 
    462    let chromeWin = this.browser.ownerGlobal;
    463    if (!chromeWin.PopupNotifications) {
    464      this.cancel();
    465      return;
    466    }
    467 
    468    // Transform the PermissionPrompt actions into PopupNotification actions.
    469    let popupNotificationActions = [];
    470    for (let promptAction of this.promptActions) {
    471      let action = {
    472        label: promptAction.label,
    473        accessKey: promptAction.accessKey,
    474        callback: state => {
    475          if (promptAction.callback) {
    476            promptAction.callback();
    477          }
    478 
    479          if (this.usePermissionManager && this.permissionKey) {
    480            if (
    481              (state && state.checkboxChecked && state.source != "esc-press") ||
    482              promptAction.scope == lazy.SitePermissions.SCOPE_PERSISTENT
    483            ) {
    484              // Permanently store permission.
    485              let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
    486              // Only remember permission for session if in PB mode.
    487              if (lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
    488                scope = lazy.SitePermissions.SCOPE_SESSION;
    489              }
    490              lazy.SitePermissions.setForPrincipal(
    491                this.principal,
    492                this.permissionKey,
    493                promptAction.action,
    494                scope
    495              );
    496            } else {
    497              lazy.SitePermissions.setForPrincipal(
    498                this.principal,
    499                this.permissionKey,
    500                promptAction.action,
    501                lazy.SitePermissions.SCOPE_TEMPORARY,
    502                this.browser
    503              );
    504            }
    505 
    506            // Grant permission if action is ALLOW.
    507            if (promptAction.action == lazy.SitePermissions.ALLOW) {
    508              this.allow();
    509            } else {
    510              this.cancel();
    511            }
    512          } else if (this.permissionKey) {
    513            lazy.SitePermissions.setForPrincipal(
    514              null,
    515              this.permissionKey,
    516              promptAction.action,
    517              lazy.SitePermissions.SCOPE_TEMPORARY,
    518              this.browser
    519            );
    520          }
    521        },
    522      };
    523      if (promptAction.dismiss) {
    524        action.dismiss = promptAction.dismiss;
    525      }
    526 
    527      popupNotificationActions.push(action);
    528    }
    529 
    530    this.#showNotification(popupNotificationActions);
    531  }
    532 
    533  postPrompt() {
    534    let browser = this.browser;
    535    let principal = this.principal;
    536    let chromeWin = browser.ownerGlobal;
    537    if (!chromeWin.PopupNotifications) {
    538      return;
    539    }
    540 
    541    if (!this.permissionKey) {
    542      throw new Error("permissionKey is required to show a post-prompt");
    543    }
    544 
    545    if (!this.postPromptActions) {
    546      throw new Error("postPromptActions are required to show a post-prompt");
    547    }
    548 
    549    // Transform the PermissionPrompt actions into PopupNotification actions.
    550    let popupNotificationActions = [];
    551    for (let promptAction of this.postPromptActions) {
    552      let action = {
    553        label: promptAction.label,
    554        accessKey: promptAction.accessKey,
    555        callback: () => {
    556          if (promptAction.callback) {
    557            promptAction.callback();
    558          }
    559 
    560          // Post-prompt permissions are stored permanently by default.
    561          // Since we can not reply to the original permission request anymore,
    562          // the page will need to listen for permission changes which are triggered
    563          // by permanent entries in the permission manager.
    564          let scope = lazy.SitePermissions.SCOPE_PERSISTENT;
    565          // Only remember permission for session if in PB mode.
    566          if (lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)) {
    567            scope = lazy.SitePermissions.SCOPE_SESSION;
    568          }
    569          lazy.SitePermissions.setForPrincipal(
    570            principal,
    571            this.permissionKey,
    572            promptAction.action,
    573            scope
    574          );
    575        },
    576      };
    577      popupNotificationActions.push(action);
    578    }
    579 
    580    // Post-prompt animation
    581    if (!chromeWin.gReduceMotion) {
    582      let anchor = chromeWin.document.getElementById(this.anchorID);
    583      // Only show the animation on the first request, not after e.g. tab switching.
    584      anchor.addEventListener(
    585        "animationend",
    586        () => anchor.removeAttribute("animate"),
    587        { once: true }
    588      );
    589      anchor.setAttribute("animate", "true");
    590    }
    591 
    592    this.#showNotification(popupNotificationActions, true);
    593  }
    594 
    595  #showNotification(actions, postPrompt = false) {
    596    let chromeWin = this.browser.ownerGlobal;
    597    let mainAction = actions.length ? actions[0] : null;
    598    let secondaryActions = actions.splice(1);
    599 
    600    let options = this.popupOptions;
    601 
    602    if (!options.hasOwnProperty("displayURI") || options.displayURI) {
    603      options.displayURI = this.principal.URI;
    604    }
    605 
    606    if (!postPrompt) {
    607      // Permission prompts are always persistent; the close button is controlled by a pref.
    608      options.persistent = true;
    609      options.hideClose = true;
    610    }
    611 
    612    options.eventCallback = (topic, nextRemovalReason, isCancel) => {
    613      // When the docshell of the browser is aboout to be swapped to another one,
    614      // the "swapping" event is called. Returning true causes the notification
    615      // to be moved to the new browser.
    616      if (topic == "swapping") {
    617        return true;
    618      }
    619      // The prompt has been shown, notify the PermissionUI.
    620      // onShown() is currently not called for post-prompts,
    621      // because there is no prompt that would make use of this.
    622      // You can remove this restriction if you need it, but be
    623      // mindful of other consumers.
    624      if (topic == "shown" && !postPrompt) {
    625        this.onShown();
    626      }
    627      // The prompt has been removed, notify the PermissionUI.
    628      // onAfterShow() is currently not called for post-prompts,
    629      // because there is no prompt that would make use of this.
    630      // You can remove this restriction if you need it, but be
    631      // mindful of other consumers.
    632      if (topic == "removed" && !postPrompt) {
    633        if (isCancel) {
    634          this.cancel();
    635        }
    636        this.onAfterShow();
    637      }
    638      return false;
    639    };
    640 
    641    options.hintText = this.hintText;
    642    // Post-prompts show up as dismissed.
    643    options.dismissed = postPrompt;
    644 
    645    // onBeforeShow() is currently not called for post-prompts,
    646    // because there is no prompt that would make use of this.
    647    // You can remove this restriction if you need it, but be
    648    // mindful of other consumers.
    649    if (postPrompt || this.onBeforeShow() !== false) {
    650      chromeWin.PopupNotifications.show(
    651        this.browser,
    652        this.notificationID,
    653        this.message,
    654        this.anchorID,
    655        mainAction,
    656        secondaryActions,
    657        options
    658      );
    659    }
    660  }
    661 }
    662 
    663 /**
    664 * A subclass of PermissionPrompt that assumes
    665 * that this.request is an nsIContentPermissionRequest
    666 * and fills in some of the required properties on the
    667 * PermissionPrompt. For callers that are wrapping an
    668 * nsIContentPermissionRequest, this should be subclassed
    669 * rather than PermissionPrompt.
    670 */
    671 class PermissionPromptForRequest extends PermissionPrompt {
    672  get browser() {
    673    // In the e10s-case, the <xul:browser> will be at request.element.
    674    // In the single-process case, we have to use some XPCOM incantations
    675    // to resolve to the <xul:browser>.
    676    if (this.request.element) {
    677      return this.request.element;
    678    }
    679    return this.request.window.docShell.chromeEventHandler;
    680  }
    681 
    682  get principal() {
    683    let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest);
    684    return request.getDelegatePrincipal(this.type);
    685  }
    686 
    687  cancel() {
    688    this.request.cancel();
    689  }
    690 
    691  allow(choices) {
    692    this.request.allow(choices);
    693  }
    694 }
    695 
    696 /**
    697 * A subclass of PermissionPromptForRequest that prompts
    698 * for a Synthetic SitePermsAddon addon type and starts a synthetic
    699 * addon install flow.
    700 */
    701 class SitePermsAddonInstallRequest extends PermissionPromptForRequest {
    702  prompt() {
    703    // fallback to regular permission prompt for localhost,
    704    // or when the SitePermsAddonProvider is not enabled.
    705    if (this.principal.isLoopbackHost || !lazy.sitePermsAddonsProviderEnabled) {
    706      super.prompt();
    707      return;
    708    }
    709 
    710    // Otherwise, we'll use the addon install flow.
    711    lazy.AddonManager.installSitePermsAddonFromWebpage(
    712      this.browser,
    713      this.principal,
    714      this.permName
    715    ).then(
    716      () => {
    717        this.allow();
    718      },
    719      err => {
    720        this.cancel();
    721 
    722        // Print an error message in the console to give more information to the developer.
    723        let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
    724        let errorMessage =
    725          this.getInstallErrorMessage(err) ||
    726          `${this.permName} access was rejected: ${err.message}`;
    727 
    728        let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
    729        scriptError.initWithWindowID(
    730          errorMessage,
    731          null,
    732          0,
    733          0,
    734          0,
    735          "content javascript",
    736          this.browser.browsingContext.currentWindowGlobal.innerWindowId
    737        );
    738        Services.console.logMessage(scriptError);
    739      }
    740    );
    741  }
    742 
    743  /**
    744   * Returns an error message that will be printed to the console given a passed Component.Exception.
    745   * This should be overriden by children classes.
    746   *
    747   * @param {Components.Exception} err
    748   * @returns {string} The error message
    749   */
    750  getInstallErrorMessage() {
    751    return null;
    752  }
    753 }
    754 
    755 /**
    756 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
    757 * the GeoLocation API.
    758 *
    759 * @param request (nsIContentPermissionRequest)
    760 *        The request for a permission from content.
    761 */
    762 class GeolocationPermissionPrompt extends PermissionPromptForRequest {
    763  constructor(request) {
    764    super();
    765    this.request = request;
    766    let types = request.types.QueryInterface(Ci.nsIArray);
    767    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
    768    if (perm.options.length) {
    769      this.systemPermissionMsg = perm.options.queryElementAt(
    770        0,
    771        Ci.nsISupportsString
    772      );
    773    }
    774  }
    775 
    776  get type() {
    777    return "geo";
    778  }
    779 
    780  get permissionKey() {
    781    return "geo";
    782  }
    783 
    784  get popupOptions() {
    785    let pref = "browser.geolocation.warning.infoURL";
    786    let options = {
    787      learnMoreURL: Services.urlFormatter.formatURLPref(pref),
    788      displayURI: false,
    789      name: this.getPrincipalName(),
    790    };
    791 
    792    // Don't offer "always remember" action in PB mode
    793    options.checkbox = {
    794      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
    795        this.browser.ownerGlobal
    796      ),
    797    };
    798 
    799    if (this.request.isRequestDelegatedToUnsafeThirdParty) {
    800      // Second name should be the third party origin
    801      options.secondName = this.getPrincipalName(this.request.principal);
    802      options.checkbox = { show: false };
    803    }
    804 
    805    if (options.checkbox.show) {
    806      options.checkbox.label = lazy.gBrowserBundle.GetStringFromName(
    807        "geolocation.remember"
    808      );
    809    }
    810 
    811    return options;
    812  }
    813 
    814  get notificationID() {
    815    return "geolocation";
    816  }
    817 
    818  get anchorID() {
    819    return "geo-notification-icon";
    820  }
    821 
    822  get message() {
    823    if (this.principal.schemeIs("file")) {
    824      return lazy.gBrowserBundle.GetStringFromName(
    825        "geolocation.shareWithFile4"
    826      );
    827    }
    828 
    829    if (this.request.isRequestDelegatedToUnsafeThirdParty) {
    830      return lazy.gBrowserBundle.formatStringFromName(
    831        "geolocation.shareWithSiteUnsafeDelegation2",
    832        ["<>", "{}"]
    833      );
    834    }
    835 
    836    return lazy.gBrowserBundle.formatStringFromName(
    837      "geolocation.shareWithSite4",
    838      ["<>"]
    839    );
    840  }
    841 
    842  get hintText() {
    843    let productName = lazy.gBrandBundle.GetStringFromName("brandShortName");
    844 
    845    if (this.systemPermissionMsg == "sysdlg") {
    846      return lazy.gBrowserBundle.formatStringFromName(
    847        "geolocation.systemWillRequestPermission",
    848        [productName]
    849      );
    850    }
    851 
    852    if (this.systemPermissionMsg == "syssetting") {
    853      return lazy.gBrowserBundle.formatStringFromName(
    854        "geolocation.needsSystemSetting",
    855        [productName]
    856      );
    857    }
    858 
    859    return undefined;
    860  }
    861 
    862  get promptActions() {
    863    return [
    864      {
    865        label: lazy.gBrowserBundle.GetStringFromName("geolocation.allow"),
    866        accessKey: lazy.gBrowserBundle.GetStringFromName(
    867          "geolocation.allow.accesskey"
    868        ),
    869        action: lazy.SitePermissions.ALLOW,
    870      },
    871      {
    872        label: lazy.gBrowserBundle.GetStringFromName("geolocation.block"),
    873        accessKey: lazy.gBrowserBundle.GetStringFromName(
    874          "geolocation.block.accesskey"
    875        ),
    876        action: lazy.SitePermissions.BLOCK,
    877      },
    878    ];
    879  }
    880 
    881  #updateGeoSharing(state) {
    882    let gBrowser = this.browser.ownerGlobal.gBrowser;
    883    if (gBrowser == null) {
    884      return;
    885    }
    886    gBrowser.updateBrowserSharing(this.browser, { geo: state });
    887 
    888    // Update last access timestamp
    889    let host;
    890    try {
    891      host = this.browser.currentURI.host;
    892    } catch (e) {
    893      return;
    894    }
    895    if (host == null || host == "") {
    896      return;
    897    }
    898    lazy.ContentPrefService2.set(
    899      this.browser.currentURI.host,
    900      "permissions.geoLocation.lastAccess",
    901      new Date().toString(),
    902      this.browser.loadContext
    903    );
    904  }
    905 
    906  allow(...args) {
    907    this.#updateGeoSharing(true);
    908    super.allow(...args);
    909  }
    910 
    911  cancel(...args) {
    912    this.#updateGeoSharing(false);
    913    super.cancel(...args);
    914  }
    915 }
    916 
    917 /**
    918 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
    919 * the WebXR API.
    920 *
    921 * @param request (nsIContentPermissionRequest)
    922 *        The request for a permission from content.
    923 */
    924 class XRPermissionPrompt extends PermissionPromptForRequest {
    925  constructor(request) {
    926    super();
    927    this.request = request;
    928  }
    929 
    930  get type() {
    931    return "xr";
    932  }
    933 
    934  get permissionKey() {
    935    return "xr";
    936  }
    937 
    938  get popupOptions() {
    939    let pref = "browser.xr.warning.infoURL";
    940    let options = {
    941      learnMoreURL: Services.urlFormatter.formatURLPref(pref),
    942      displayURI: false,
    943      name: this.getPrincipalName(),
    944    };
    945 
    946    // Don't offer "always remember" action in PB mode
    947    options.checkbox = {
    948      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
    949        this.browser.ownerGlobal
    950      ),
    951    };
    952 
    953    if (options.checkbox.show) {
    954      options.checkbox.label =
    955        lazy.gBrowserBundle.GetStringFromName("xr.remember");
    956    }
    957 
    958    return options;
    959  }
    960 
    961  get notificationID() {
    962    return "xr";
    963  }
    964 
    965  get anchorID() {
    966    return "xr-notification-icon";
    967  }
    968 
    969  get message() {
    970    if (this.principal.schemeIs("file")) {
    971      return lazy.gBrowserBundle.GetStringFromName("xr.shareWithFile4");
    972    }
    973 
    974    return lazy.gBrowserBundle.formatStringFromName("xr.shareWithSite4", [
    975      "<>",
    976    ]);
    977  }
    978 
    979  get promptActions() {
    980    return [
    981      {
    982        label: lazy.gBrowserBundle.GetStringFromName("xr.allow2"),
    983        accessKey: lazy.gBrowserBundle.GetStringFromName("xr.allow2.accesskey"),
    984        action: lazy.SitePermissions.ALLOW,
    985      },
    986      {
    987        label: lazy.gBrowserBundle.GetStringFromName("xr.block"),
    988        accessKey: lazy.gBrowserBundle.GetStringFromName("xr.block.accesskey"),
    989        action: lazy.SitePermissions.BLOCK,
    990      },
    991    ];
    992  }
    993 
    994  #updateXRSharing(state) {
    995    let gBrowser = this.browser.ownerGlobal.gBrowser;
    996    if (gBrowser == null) {
    997      return;
    998    }
    999    gBrowser.updateBrowserSharing(this.browser, { xr: state });
   1000 
   1001    let devicePermOrigins = this.browser.getDevicePermissionOrigins("xr");
   1002    if (!state) {
   1003      devicePermOrigins.delete(this.principal.origin);
   1004      return;
   1005    }
   1006    devicePermOrigins.add(this.principal.origin);
   1007  }
   1008 
   1009  allow(...args) {
   1010    this.#updateXRSharing(true);
   1011    super.allow(...args);
   1012  }
   1013 
   1014  cancel(...args) {
   1015    this.#updateXRSharing(false);
   1016    super.cancel(...args);
   1017  }
   1018 }
   1019 
   1020 /**
   1021 * Base class for Local Network Access (LNA) permission prompts.
   1022 * Provides automatic timeout handling for LNA prompts.
   1023 *
   1024 * If the user doesn't respond to the prompt within the timeout period,
   1025 * the prompt is automatically cancelled and the network request fails.
   1026 */
   1027 class LNAPermissionPromptBase extends PermissionPromptForRequest {
   1028  static DEFAULT_PROMPT_TIMEOUT_MS = 300000;
   1029 
   1030  #timeoutTimer = null;
   1031 
   1032  constructor(request) {
   1033    super();
   1034    this.request = request;
   1035  }
   1036 
   1037  onBeforeShow() {
   1038    // Notify LNAPermissionRequest that the prompt is being shown.
   1039    // This triggers telemetry recording and notifies nsHttpChannel.
   1040    if (typeof this.request.notifyShown === "function") {
   1041      this.request.notifyShown();
   1042    }
   1043    return true;
   1044  }
   1045 
   1046  onShown() {
   1047    this.#startTimeoutTimer();
   1048  }
   1049 
   1050  onAfterShow() {
   1051    this.#clearTimeoutTimer();
   1052  }
   1053 
   1054  cancel() {
   1055    super.cancel();
   1056  }
   1057 
   1058  allow(choices) {
   1059    super.allow(choices);
   1060  }
   1061 
   1062  #startTimeoutTimer() {
   1063    this.#clearTimeoutTimer();
   1064 
   1065    this.#timeoutTimer = lazy.setTimeout(() => {
   1066      let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
   1067        Ci.nsIScriptError
   1068      );
   1069      scriptError.initWithWindowID(
   1070        `LNA permission prompt timed out after ${lazy.lnaPromptTimeoutMs / 1000} seconds`,
   1071        null,
   1072        0,
   1073        0,
   1074        Ci.nsIScriptError.warningFlag,
   1075        "content javascript",
   1076        this.browser.browsingContext.currentWindowGlobal.innerWindowId
   1077      );
   1078      Services.console.logMessage(scriptError);
   1079 
   1080      this.#removePrompt();
   1081      this.cancel();
   1082    }, lazy.lnaPromptTimeoutMs);
   1083  }
   1084 
   1085  #removePrompt() {
   1086    let chromeWin = this.browser?.ownerGlobal;
   1087    let notification = chromeWin?.PopupNotifications.getNotification(
   1088      this.notificationID,
   1089      this.browser
   1090    );
   1091    if (notification) {
   1092      chromeWin.PopupNotifications.remove(notification);
   1093    }
   1094  }
   1095 
   1096  #clearTimeoutTimer() {
   1097    if (this.#timeoutTimer) {
   1098      lazy.clearTimeout(this.#timeoutTimer);
   1099      this.#timeoutTimer = null;
   1100    }
   1101  }
   1102 }
   1103 
   1104 /**
   1105 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
   1106 * the Local Host Access.
   1107 *
   1108 * @param request (nsIContentPermissionRequest)
   1109 *        The request for a permission from content.
   1110 */
   1111 class LocalHostPermissionPrompt extends LNAPermissionPromptBase {
   1112  get type() {
   1113    return "localhost";
   1114  }
   1115 
   1116  get permissionKey() {
   1117    return "localhost";
   1118  }
   1119 
   1120  get popupOptions() {
   1121    let options = {
   1122      learnMoreURL: Services.urlFormatter.formatURLPref(
   1123        "browser.lna.warning.infoURL"
   1124      ),
   1125      displayURI: false,
   1126      name: this.getPrincipalName(),
   1127    };
   1128 
   1129    // Don't offer "always remember" action in PB mode
   1130    options.checkbox = {
   1131      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
   1132        this.browser.ownerGlobal
   1133      ),
   1134    };
   1135 
   1136    if (this.request.isRequestDelegatedToUnsafeThirdParty) {
   1137      // Second name should be the third party origin
   1138      options.secondName = this.getPrincipalName(this.request.principal);
   1139      options.checkbox = { show: false };
   1140    }
   1141 
   1142    if (options.checkbox.show) {
   1143      options.checkbox.label = lazy.gBrowserBundle.GetStringFromName(
   1144        "localhost.remember2"
   1145      );
   1146    }
   1147 
   1148    return options;
   1149  }
   1150 
   1151  get notificationID() {
   1152    return "localhost";
   1153  }
   1154 
   1155  get anchorID() {
   1156    return "localhost-notification-icon";
   1157  }
   1158 
   1159  get message() {
   1160    return lazy.gBrowserBundle.formatStringFromName(
   1161      "localhost.allowWithSite2",
   1162      ["<>"]
   1163    );
   1164  }
   1165 
   1166  get promptActions() {
   1167    return [
   1168      {
   1169        label: lazy.gBrowserBundle.GetStringFromName("localhost.allowlabel"),
   1170        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1171          "localhost.allow.accesskey"
   1172        ),
   1173        action: lazy.SitePermissions.ALLOW,
   1174      },
   1175      {
   1176        label: lazy.gBrowserBundle.GetStringFromName("localhost.blocklabel"),
   1177        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1178          "localhost.block.accesskey"
   1179        ),
   1180        action: lazy.SitePermissions.BLOCK,
   1181      },
   1182    ];
   1183  }
   1184 }
   1185 
   1186 /**
   1187 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
   1188 * the Desktop Notification API.
   1189 *
   1190 * @param request (nsIContentPermissionRequest)
   1191 *        The request for a permission from content.
   1192 * @return {PermissionPrompt} (see documentation in header)
   1193 */
   1194 class DesktopNotificationPermissionPrompt extends PermissionPromptForRequest {
   1195  constructor(request) {
   1196    super();
   1197    this.request = request;
   1198 
   1199    XPCOMUtils.defineLazyPreferenceGetter(
   1200      this,
   1201      "requiresUserInput",
   1202      "dom.webnotifications.requireuserinteraction"
   1203    );
   1204    XPCOMUtils.defineLazyPreferenceGetter(
   1205      this,
   1206      "postPromptEnabled",
   1207      "permissions.desktop-notification.postPrompt.enabled"
   1208    );
   1209    XPCOMUtils.defineLazyPreferenceGetter(
   1210      this,
   1211      "notNowEnabled",
   1212      "permissions.desktop-notification.notNow.enabled"
   1213    );
   1214  }
   1215 
   1216  get type() {
   1217    return "desktop-notification";
   1218  }
   1219 
   1220  get permissionKey() {
   1221    return "desktop-notification";
   1222  }
   1223 
   1224  get popupOptions() {
   1225    let learnMoreURL =
   1226      Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
   1227 
   1228    return {
   1229      learnMoreURL,
   1230      displayURI: false,
   1231      name: this.getPrincipalName(),
   1232    };
   1233  }
   1234 
   1235  get notificationID() {
   1236    return "web-notifications";
   1237  }
   1238 
   1239  get anchorID() {
   1240    return "web-notifications-notification-icon";
   1241  }
   1242 
   1243  get message() {
   1244    return lazy.gBrowserBundle.formatStringFromName(
   1245      "webNotifications.receiveFromSite3",
   1246      ["<>"]
   1247    );
   1248  }
   1249 
   1250  get promptActions() {
   1251    let actions = [
   1252      {
   1253        label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
   1254        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1255          "webNotifications.allow2.accesskey"
   1256        ),
   1257        action: lazy.SitePermissions.ALLOW,
   1258        scope: lazy.SitePermissions.SCOPE_PERSISTENT,
   1259      },
   1260    ];
   1261    if (this.notNowEnabled) {
   1262      actions.push({
   1263        label: lazy.gBrowserBundle.GetStringFromName("webNotifications.notNow"),
   1264        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1265          "webNotifications.notNow.accesskey"
   1266        ),
   1267        action: lazy.SitePermissions.BLOCK,
   1268      });
   1269    }
   1270 
   1271    let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
   1272      this.browser
   1273    );
   1274    actions.push({
   1275      label: isBrowserPrivate
   1276        ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
   1277        : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
   1278      accessKey: isBrowserPrivate
   1279        ? lazy.gBrowserBundle.GetStringFromName(
   1280            "webNotifications.block.accesskey"
   1281          )
   1282        : lazy.gBrowserBundle.GetStringFromName(
   1283            "webNotifications.alwaysBlock.accesskey"
   1284          ),
   1285      action: lazy.SitePermissions.BLOCK,
   1286      scope: isBrowserPrivate
   1287        ? lazy.SitePermissions.SCOPE_SESSION
   1288        : lazy.SitePermissions.SCOPE_PERSISTENT,
   1289    });
   1290    return actions;
   1291  }
   1292 
   1293  get postPromptActions() {
   1294    let actions = [
   1295      {
   1296        label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"),
   1297        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1298          "webNotifications.allow2.accesskey"
   1299        ),
   1300        action: lazy.SitePermissions.ALLOW,
   1301      },
   1302    ];
   1303 
   1304    let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
   1305      this.browser
   1306    );
   1307    actions.push({
   1308      label: isBrowserPrivate
   1309        ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block")
   1310        : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"),
   1311      accessKey: isBrowserPrivate
   1312        ? lazy.gBrowserBundle.GetStringFromName(
   1313            "webNotifications.block.accesskey"
   1314          )
   1315        : lazy.gBrowserBundle.GetStringFromName(
   1316            "webNotifications.alwaysBlock.accesskey"
   1317          ),
   1318      action: lazy.SitePermissions.BLOCK,
   1319    });
   1320    return actions;
   1321  }
   1322 }
   1323 
   1324 /**
   1325 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
   1326 * the Local Network Access.
   1327 *
   1328 * @param request (nsIContentPermissionRequest)
   1329 *        The request for a permission from content.
   1330 */
   1331 class LocalNetworkPermissionPrompt extends LNAPermissionPromptBase {
   1332  get type() {
   1333    return "local-network";
   1334  }
   1335 
   1336  get permissionKey() {
   1337    return "local-network";
   1338  }
   1339 
   1340  get popupOptions() {
   1341    let options = {
   1342      learnMoreURL: Services.urlFormatter.formatURLPref(
   1343        "browser.lna.warning.infoURL"
   1344      ),
   1345      displayURI: false,
   1346      name: this.getPrincipalName(),
   1347    };
   1348 
   1349    // Don't offer "always remember" action in PB mode
   1350    options.checkbox = {
   1351      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
   1352        this.browser.ownerGlobal
   1353      ),
   1354    };
   1355 
   1356    if (this.request.isRequestDelegatedToUnsafeThirdParty) {
   1357      // Second name should be the third party origin
   1358      options.secondName = this.getPrincipalName(this.request.principal);
   1359      options.checkbox = { show: false };
   1360    }
   1361 
   1362    if (options.checkbox.show) {
   1363      options.checkbox.label = lazy.gBrowserBundle.GetStringFromName(
   1364        "localNetwork.remember2"
   1365      );
   1366    }
   1367 
   1368    return options;
   1369  }
   1370 
   1371  get notificationID() {
   1372    return "local-network";
   1373  }
   1374 
   1375  get anchorID() {
   1376    return "local-network-notification-icon";
   1377  }
   1378 
   1379  get message() {
   1380    return lazy.gBrowserBundle.formatStringFromName(
   1381      "localNetwork.allowWithSite2",
   1382      ["<>"]
   1383    );
   1384  }
   1385 
   1386  get promptActions() {
   1387    return [
   1388      {
   1389        label: lazy.gBrowserBundle.GetStringFromName("localNetwork.allowLabel"),
   1390        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1391          "localNetwork.allow.accesskey"
   1392        ),
   1393        action: lazy.SitePermissions.ALLOW,
   1394      },
   1395      {
   1396        label: lazy.gBrowserBundle.GetStringFromName("localNetwork.blockLabel"),
   1397        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1398          "localNetwork.block.accesskey"
   1399        ),
   1400        action: lazy.SitePermissions.BLOCK,
   1401      },
   1402    ];
   1403  }
   1404 }
   1405 /**
   1406 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
   1407 * the persistent-storage API.
   1408 *
   1409 * @param request (nsIContentPermissionRequest)
   1410 *        The request for a permission from content.
   1411 */
   1412 class PersistentStoragePermissionPrompt extends PermissionPromptForRequest {
   1413  constructor(request) {
   1414    super();
   1415    this.request = request;
   1416  }
   1417 
   1418  get type() {
   1419    return "persistent-storage";
   1420  }
   1421 
   1422  get permissionKey() {
   1423    return "persistent-storage";
   1424  }
   1425 
   1426  get popupOptions() {
   1427    let learnMoreURL =
   1428      Services.urlFormatter.formatURLPref("app.support.baseURL") +
   1429      "storage-permissions";
   1430    let options = {
   1431      learnMoreURL,
   1432      displayURI: false,
   1433      name: this.getPrincipalName(),
   1434    };
   1435 
   1436    options.checkbox = {
   1437      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
   1438        this.browser.ownerGlobal
   1439      ),
   1440    };
   1441 
   1442    if (options.checkbox.show) {
   1443      options.checkbox.label = lazy.gFluentStrings.formatValueSync(
   1444        "perm-persistent-storage-remember"
   1445      );
   1446    }
   1447 
   1448    return options;
   1449  }
   1450 
   1451  get notificationID() {
   1452    return "persistent-storage";
   1453  }
   1454 
   1455  get anchorID() {
   1456    return "persistent-storage-notification-icon";
   1457  }
   1458 
   1459  get message() {
   1460    return lazy.gBrowserBundle.formatStringFromName(
   1461      "persistentStorage.allowWithSite2",
   1462      ["<>"]
   1463    );
   1464  }
   1465 
   1466  get promptActions() {
   1467    return [
   1468      {
   1469        label: lazy.gBrowserBundle.GetStringFromName("persistentStorage.allow"),
   1470        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1471          "persistentStorage.allow.accesskey"
   1472        ),
   1473        action: Ci.nsIPermissionManager.ALLOW_ACTION,
   1474        scope: lazy.SitePermissions.SCOPE_PERSISTENT,
   1475      },
   1476      {
   1477        label: lazy.gBrowserBundle.GetStringFromName(
   1478          "persistentStorage.block.label"
   1479        ),
   1480        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1481          "persistentStorage.block.accesskey"
   1482        ),
   1483        action: lazy.SitePermissions.BLOCK,
   1484      },
   1485    ];
   1486  }
   1487 }
   1488 
   1489 /**
   1490 * Creates a PermissionPrompt for a nsIContentPermissionRequest for
   1491 * the WebMIDI API.
   1492 *
   1493 * @param request (nsIContentPermissionRequest)
   1494 *        The request for a permission from content.
   1495 */
   1496 class MIDIPermissionPrompt extends SitePermsAddonInstallRequest {
   1497  constructor(request) {
   1498    super();
   1499    this.request = request;
   1500    let types = request.types.QueryInterface(Ci.nsIArray);
   1501    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
   1502    this.isSysexPerm =
   1503      !!perm.options.length &&
   1504      perm.options.queryElementAt(0, Ci.nsISupportsString) == "sysex";
   1505    this.permName = "midi";
   1506    if (this.isSysexPerm) {
   1507      this.permName = "midi-sysex";
   1508    }
   1509  }
   1510 
   1511  get type() {
   1512    return "midi";
   1513  }
   1514 
   1515  get permissionKey() {
   1516    return this.permName;
   1517  }
   1518 
   1519  get popupOptions() {
   1520    // TODO (bug 1433235) We need a security/permissions explanation URL for this
   1521    let options = {
   1522      displayURI: false,
   1523      name: this.getPrincipalName(),
   1524    };
   1525 
   1526    // Don't offer "always remember" action in PB mode
   1527    options.checkbox = {
   1528      show: !lazy.PrivateBrowsingUtils.isWindowPrivate(
   1529        this.browser.ownerGlobal
   1530      ),
   1531    };
   1532 
   1533    if (options.checkbox.show) {
   1534      options.checkbox.label =
   1535        lazy.gBrowserBundle.GetStringFromName("midi.remember");
   1536    }
   1537 
   1538    return options;
   1539  }
   1540 
   1541  get notificationID() {
   1542    return "midi";
   1543  }
   1544 
   1545  get anchorID() {
   1546    return "midi-notification-icon";
   1547  }
   1548 
   1549  get message() {
   1550    let message;
   1551    if (this.principal.schemeIs("file")) {
   1552      if (this.isSysexPerm) {
   1553        message = lazy.gBrowserBundle.GetStringFromName(
   1554          "midi.shareSysexWithFile"
   1555        );
   1556      } else {
   1557        message = lazy.gBrowserBundle.GetStringFromName("midi.shareWithFile");
   1558      }
   1559    } else if (this.isSysexPerm) {
   1560      message = lazy.gBrowserBundle.formatStringFromName(
   1561        "midi.shareSysexWithSite",
   1562        ["<>"]
   1563      );
   1564    } else {
   1565      message = lazy.gBrowserBundle.formatStringFromName("midi.shareWithSite", [
   1566        "<>",
   1567      ]);
   1568    }
   1569    return message;
   1570  }
   1571 
   1572  get promptActions() {
   1573    return [
   1574      {
   1575        label: lazy.gBrowserBundle.GetStringFromName("midi.allow.label"),
   1576        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1577          "midi.allow.accesskey"
   1578        ),
   1579        action: Ci.nsIPermissionManager.ALLOW_ACTION,
   1580      },
   1581      {
   1582        label: lazy.gBrowserBundle.GetStringFromName("midi.block.label"),
   1583        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1584          "midi.block.accesskey"
   1585        ),
   1586        action: Ci.nsIPermissionManager.DENY_ACTION,
   1587      },
   1588    ];
   1589  }
   1590 
   1591  /**
   1592   * @override
   1593   * @param {Components.Exception} err
   1594   * @returns {string}
   1595   */
   1596  getInstallErrorMessage(err) {
   1597    return `WebMIDI access request was denied: ❝${err.message}❞. See https://developer.mozilla.org/docs/Web/API/Navigator/requestMIDIAccess for more information`;
   1598  }
   1599 }
   1600 
   1601 class StorageAccessPermissionPrompt extends PermissionPromptForRequest {
   1602  #permissionKey;
   1603 
   1604  constructor(request) {
   1605    super();
   1606    this.request = request;
   1607    this.siteOption = null;
   1608    this.#permissionKey = `3rdPartyStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.origin}`;
   1609 
   1610    let types = this.request.types.QueryInterface(Ci.nsIArray);
   1611    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
   1612    let options = perm.options.QueryInterface(Ci.nsIArray);
   1613    // If we have an option, the permission request is different in some way.
   1614    // We may be in a call from requestStorageAccessUnderSite or a frame-scoped
   1615    // request, which means that the embedding principal is not the current top-level
   1616    // or the permission key is different.
   1617    if (options.length != 2) {
   1618      return;
   1619    }
   1620 
   1621    let topLevelOption = options.queryElementAt(0, Ci.nsISupportsString).data;
   1622    if (topLevelOption) {
   1623      this.siteOption = topLevelOption;
   1624    }
   1625    let frameOption = options.queryElementAt(1, Ci.nsISupportsString).data;
   1626    if (frameOption) {
   1627      // We replace the permission key with a frame-specific one that only has a site after the delimiter
   1628      this.#permissionKey = `3rdPartyFrameStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.siteOrigin}`;
   1629    }
   1630  }
   1631 
   1632  get usePermissionManager() {
   1633    return false;
   1634  }
   1635 
   1636  get type() {
   1637    return "storage-access";
   1638  }
   1639 
   1640  get permissionKey() {
   1641    // Make sure this name is unique per each third-party tracker
   1642    return this.#permissionKey;
   1643  }
   1644 
   1645  get temporaryPermissionURI() {
   1646    if (this.siteOption) {
   1647      return Services.io.newURI(this.siteOption);
   1648    }
   1649    return undefined;
   1650  }
   1651 
   1652  prettifyHostPort(hostport) {
   1653    let [host, port] = hostport.split(":");
   1654    host = lazy.IDNService.convertToDisplayIDN(host);
   1655    if (port) {
   1656      return `${host}:${port}`;
   1657    }
   1658    return host;
   1659  }
   1660 
   1661  get popupOptions() {
   1662    let learnMoreURL =
   1663      Services.urlFormatter.formatURLPref("app.support.baseURL") +
   1664      "third-party-cookies";
   1665    let hostPort = this.prettifyHostPort(this.principal.hostPort);
   1666    let hintText = lazy.gBrowserBundle.formatStringFromName(
   1667      "storageAccess1.hintText",
   1668      [hostPort]
   1669    );
   1670    return {
   1671      learnMoreURL,
   1672      displayURI: false,
   1673      hintText,
   1674      escAction: "secondarybuttoncommand",
   1675    };
   1676  }
   1677 
   1678  get notificationID() {
   1679    return "storage-access";
   1680  }
   1681 
   1682  get anchorID() {
   1683    return "storage-access-notification-icon";
   1684  }
   1685 
   1686  get message() {
   1687    let embeddingHost = this.topLevelPrincipal.host;
   1688 
   1689    if (this.siteOption) {
   1690      embeddingHost = this.siteOption.split("://").at(-1);
   1691    }
   1692 
   1693    return lazy.gBrowserBundle.formatStringFromName("storageAccess4.message", [
   1694      this.prettifyHostPort(this.principal.hostPort),
   1695      this.prettifyHostPort(embeddingHost),
   1696    ]);
   1697  }
   1698 
   1699  get promptActions() {
   1700    let self = this;
   1701 
   1702    return [
   1703      {
   1704        label: lazy.gBrowserBundle.GetStringFromName(
   1705          "storageAccess1.Allow.label"
   1706        ),
   1707        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1708          "storageAccess1.Allow.accesskey"
   1709        ),
   1710        action: Ci.nsIPermissionManager.ALLOW_ACTION,
   1711        callback() {
   1712          self.allow({ "storage-access": "allow" });
   1713        },
   1714      },
   1715      {
   1716        label: lazy.gBrowserBundle.GetStringFromName(
   1717          "storageAccess1.DontAllow.label"
   1718        ),
   1719        accessKey: lazy.gBrowserBundle.GetStringFromName(
   1720          "storageAccess1.DontAllow.accesskey"
   1721        ),
   1722        action: Ci.nsIPermissionManager.DENY_ACTION,
   1723        callback() {
   1724          self.cancel();
   1725        },
   1726      },
   1727    ];
   1728  }
   1729 
   1730  get topLevelPrincipal() {
   1731    return this.request.topLevelPrincipal;
   1732  }
   1733 }
   1734 
   1735 export const PermissionUI = {
   1736  PermissionPromptForRequest,
   1737  GeolocationPermissionPrompt,
   1738  XRPermissionPrompt,
   1739  DesktopNotificationPermissionPrompt,
   1740  PersistentStoragePermissionPrompt,
   1741  MIDIPermissionPrompt,
   1742  StorageAccessPermissionPrompt,
   1743  LocalHostPermissionPrompt,
   1744  LocalNetworkPermissionPrompt,
   1745 };