tor-browser

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

WebRTCParent.sys.mjs (57646B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     11  SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
     12  webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
     13 });
     14 
     15 XPCOMUtils.defineLazyServiceGetter(
     16  lazy,
     17  "OSPermissions",
     18  "@mozilla.org/ospermissionrequest;1",
     19  Ci.nsIOSPermissionRequest
     20 );
     21 
     22 export class WebRTCParent extends JSWindowActorParent {
     23  didDestroy() {
     24    // Media stream tracks end on unload, so call stopRecording() on them early
     25    // *before* we go away, to ensure we're working with the right principal.
     26    this.stopRecording(this.manager.outerWindowId);
     27    lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
     28    // Must clear activePerms here to prevent them from being read by laggard
     29    // stopRecording() calls, which due to IPC, may come in *after* navigation.
     30    // This is to prevent granting temporary grace periods to the wrong page.
     31    lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
     32  }
     33 
     34  getBrowser() {
     35    return this.browsingContext.top.embedderElement;
     36  }
     37 
     38  receiveMessage(aMessage) {
     39    switch (aMessage.name) {
     40      case "rtcpeer:Request": {
     41        let params = Object.freeze(
     42          Object.assign(
     43            {
     44              origin: this.manager.documentPrincipal.origin,
     45            },
     46            aMessage.data
     47          )
     48        );
     49 
     50        let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
     51 
     52        (async function () {
     53          for (let blocker of blockers) {
     54            try {
     55              let result = await blocker(params);
     56              if (result == "deny") {
     57                return false;
     58              }
     59            } catch (err) {
     60              console.error(`error in PeerConnection blocker: ${err.message}`);
     61            }
     62          }
     63          return true;
     64        })().then(decision => {
     65          let message;
     66          if (decision) {
     67            lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
     68            message = "rtcpeer:Allow";
     69          } else {
     70            lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
     71            message = "rtcpeer:Deny";
     72          }
     73 
     74          this.sendAsyncMessage(message, {
     75            callID: params.callID,
     76            windowID: params.windowID,
     77          });
     78        });
     79        break;
     80      }
     81      case "rtcpeer:CancelRequest": {
     82        let params = Object.freeze({
     83          origin: this.manager.documentPrincipal.origin,
     84          callID: aMessage.data,
     85        });
     86        lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
     87        break;
     88      }
     89      case "webrtc:Request": {
     90        let data = aMessage.data;
     91 
     92        // Record third party origins for telemetry.
     93        let isThirdPartyOrigin =
     94          this.manager.documentPrincipal.origin !=
     95          this.manager.topWindowContext.documentPrincipal.origin;
     96        data.isThirdPartyOrigin = isThirdPartyOrigin;
     97 
     98        data.origin = this.manager.topWindowContext.documentPrincipal.origin;
     99 
    100        let browser = this.getBrowser();
    101        if (browser.fxrPermissionPrompt) {
    102          // For Firefox Reality on Desktop, switch to a different mechanism to
    103          // prompt the user since fewer permissions are available and since many
    104          // UI dependencies are not available.
    105          browser.fxrPermissionPrompt(data);
    106        } else {
    107          prompt(this, this.getBrowser(), data);
    108        }
    109        break;
    110      }
    111      case "webrtc:StopRecording":
    112        this.stopRecording(
    113          aMessage.data.windowID,
    114          aMessage.data.mediaSource,
    115          aMessage.data.rawID
    116        );
    117        break;
    118      case "webrtc:CancelRequest": {
    119        let browser = this.getBrowser();
    120        // browser can be null when closing the window
    121        if (browser) {
    122          removePrompt(browser, aMessage.data);
    123        }
    124        break;
    125      }
    126      case "webrtc:UpdateIndicators": {
    127        let { data } = aMessage;
    128        data.documentURI = this.manager.documentURI?.spec;
    129        if (data.windowId) {
    130          if (!data.remove) {
    131            data.principal = this.manager.topWindowContext.documentPrincipal;
    132          }
    133          lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
    134        }
    135        this.updateIndicators(data);
    136        break;
    137      }
    138    }
    139  }
    140 
    141  updateIndicators(aData) {
    142    let browsingContext = this.browsingContext;
    143    let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
    144 
    145    let browser = this.getBrowser();
    146    if (!browser) {
    147      return;
    148    }
    149 
    150    state.browsingContext = browsingContext;
    151    state.windowId = aData.windowId;
    152 
    153    let tabbrowser = browser.ownerGlobal.gBrowser;
    154    if (tabbrowser) {
    155      tabbrowser.updateBrowserSharing(browser, {
    156        webRTC: state,
    157      });
    158    }
    159  }
    160 
    161  denyRequest(aRequest) {
    162    this.sendAsyncMessage("webrtc:Deny", {
    163      callID: aRequest.callID,
    164      windowID: aRequest.windowID,
    165    });
    166  }
    167 
    168  //
    169  // Deny the request because the browser does not have access to the
    170  // camera or microphone due to OS security restrictions. The user may
    171  // have granted camera/microphone access to the site, but not have
    172  // allowed the browser access in OS settings.
    173  //
    174  denyRequestNoPermission(aRequest) {
    175    this.sendAsyncMessage("webrtc:Deny", {
    176      callID: aRequest.callID,
    177      windowID: aRequest.windowID,
    178      noOSPermission: true,
    179    });
    180  }
    181 
    182  //
    183  // Check if we have permission to access the camera or screen-sharing and/or
    184  // microphone at the OS level. Triggers a request to access the device if access
    185  // is needed and the permission state has not yet been determined.
    186  //
    187  async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
    188    // Don't trigger OS permission requests for fake devices. Fake devices don't
    189    // require OS permission and the dialogs are problematic in automated testing
    190    // (where fake devices are used) because they require user interaction.
    191    if (
    192      !scrNeeded &&
    193      Services.prefs.getBoolPref("media.navigator.streams.fake", false)
    194    ) {
    195      return true;
    196    }
    197    let camStatus = {},
    198      micStatus = {};
    199    if (camNeeded || micNeeded) {
    200      lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
    201    }
    202    if (camNeeded) {
    203      let camPermission = camStatus.value;
    204      let camAccessible = await this.checkAndGetOSPermission(
    205        camPermission,
    206        lazy.OSPermissions.requestVideoCapturePermission
    207      );
    208      if (!camAccessible) {
    209        return false;
    210      }
    211    }
    212    if (micNeeded) {
    213      let micPermission = micStatus.value;
    214      let micAccessible = await this.checkAndGetOSPermission(
    215        micPermission,
    216        lazy.OSPermissions.requestAudioCapturePermission
    217      );
    218      if (!micAccessible) {
    219        return false;
    220      }
    221    }
    222    let scrStatus = {};
    223    if (scrNeeded) {
    224      lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
    225      if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
    226        lazy.OSPermissions.maybeRequestScreenCapturePermission();
    227        return false;
    228      }
    229    }
    230    return true;
    231  }
    232 
    233  //
    234  // Given a device's permission, return true if the device is accessible. If
    235  // the device's permission is not yet determined, request access to the device.
    236  // |requestPermissionFunc| must return a promise that resolves with true
    237  // if the device is accessible and false otherwise.
    238  //
    239  async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
    240    if (
    241      devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
    242      devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
    243    ) {
    244      return false;
    245    }
    246    if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
    247      let deviceAllowed = await requestPermissionFunc();
    248      if (!deviceAllowed) {
    249        return false;
    250      }
    251    }
    252    return true;
    253  }
    254 
    255  stopRecording(aOuterWindowId, aMediaSource, aRawId) {
    256    for (let { browsingContext, state } of lazy.webrtcUI._streams) {
    257      if (browsingContext == this.browsingContext) {
    258        let { principal } = state;
    259        for (let { mediaSource, rawId } of state.devices) {
    260          if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
    261            continue;
    262          }
    263          // Deactivate this device (no aRawId means all devices).
    264          this.deactivateDevicePerm(
    265            aOuterWindowId,
    266            mediaSource,
    267            rawId,
    268            principal
    269          );
    270        }
    271      }
    272    }
    273  }
    274 
    275  /**
    276   * Add a device record to webrtcUI.activePerms, denoting a device as in use.
    277   * Important to call for permission grace periods to work correctly.
    278   */
    279  activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
    280    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
    281      lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
    282    }
    283    lazy.webrtcUI.activePerms
    284      .get(this.manager.outerWindowId)
    285      .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
    286  }
    287 
    288  /**
    289   * Remove a device record from webrtcUI.activePerms, denoting a device as
    290   * no longer in use by the site. Meaning: gUM requests for this device will
    291   * no longer be implicitly granted through the webrtcUI.activePerms mechanism.
    292   *
    293   * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
    294   * grant is extended for an additional period of time through SitePermissions.
    295   */
    296  deactivateDevicePerm(
    297    aOuterWindowId,
    298    aMediaSource,
    299    aId,
    300    aPermissionPrincipal
    301  ) {
    302    // If we don't have active permissions for the given window anymore don't
    303    // set a grace period. This happens if there has been a user revoke and
    304    // webrtcUI clears the permissions.
    305    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
    306      return;
    307    }
    308    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
    309    map.delete(aOuterWindowId + aMediaSource + aId);
    310 
    311    // Add a permission grace period for camera and microphone only
    312    if (
    313      (aMediaSource != "camera" && aMediaSource != "microphone") ||
    314      !this.browsingContext.top.embedderElement
    315    ) {
    316      return;
    317    }
    318    let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
    319    if (gracePeriodMs > 0) {
    320      // A grace period is extended (even past navigation) to this outer window
    321      // + origin + deviceId only. This avoids re-prompting without the user
    322      // having to persist permission to the site, in a common case of a web
    323      // conference asking them for the camera in a lobby page, before
    324      // navigating to the actual meeting room page. Does not survive tab close.
    325      //
    326      // Caution: since navigation causes deactivation, we may be in the middle
    327      // of one. We must pass in a principal & URI for SitePermissions to use
    328      // instead of browser.currentURI, because the latter may point to a new
    329      // page already, and we must not leak permission to unrelated pages.
    330      //
    331      let permissionName = [aMediaSource, aId].join("^");
    332      lazy.SitePermissions.setForPrincipal(
    333        aPermissionPrincipal,
    334        permissionName,
    335        lazy.SitePermissions.ALLOW,
    336        lazy.SitePermissions.SCOPE_TEMPORARY,
    337        this.browsingContext.top.embedderElement,
    338        gracePeriodMs
    339      );
    340    }
    341  }
    342 
    343  /**
    344   * Checks if the principal has sufficient permissions
    345   * to fulfill the given request. If the request can be
    346   * fulfilled, a message is sent to the child
    347   * signaling that WebRTC permissions were given and
    348   * this function will return true.
    349   */
    350  checkRequestAllowed(aRequest, aPrincipal) {
    351    if (!aRequest.secure) {
    352      return false;
    353    }
    354    // Always prompt for screen sharing
    355    if (aRequest.sharingScreen) {
    356      return false;
    357    }
    358    let {
    359      callID,
    360      windowID,
    361      audioInputDevices,
    362      videoInputDevices,
    363      audioOutputDevices,
    364      hasInherentAudioConstraints,
    365      hasInherentVideoConstraints,
    366      audioOutputId,
    367    } = aRequest;
    368 
    369    if (audioOutputDevices?.length) {
    370      // Prompt if a specific device is not requested, available and allowed.
    371      let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
    372      if (
    373        !device ||
    374        !lazy.SitePermissions.getForPrincipal(
    375          aPrincipal,
    376          ["speaker", device.id].join("^"),
    377          this.getBrowser()
    378        ).state == lazy.SitePermissions.ALLOW
    379      ) {
    380        return false;
    381      }
    382      this.sendAsyncMessage("webrtc:Allow", {
    383        callID,
    384        windowID,
    385        devices: [device.deviceIndex],
    386      });
    387      return true;
    388    }
    389 
    390    let { perms } = Services;
    391    if (
    392      perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
    393    ) {
    394      perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
    395    }
    396 
    397    // Don't use persistent permissions from the top-level principal
    398    // if we're handling a potentially insecure third party
    399    // through a wildcard ("*") allow attribute.
    400    let limited = aRequest.secondOrigin;
    401 
    402    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
    403    // We consider a camera or mic active if it is active or was active within a
    404    // grace period of milliseconds ago.
    405    const isAllowed = ({ mediaSource, rawId }, permissionID) =>
    406      map?.get(windowID + mediaSource + rawId) ||
    407      (!limited &&
    408        (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
    409          lazy.SitePermissions.ALLOW ||
    410          lazy.SitePermissions.getForPrincipal(
    411            aPrincipal,
    412            [mediaSource, rawId].join("^"),
    413            this.getBrowser()
    414          ).state == lazy.SitePermissions.ALLOW));
    415 
    416    let microphone;
    417    if (audioInputDevices.length) {
    418      for (let device of audioInputDevices) {
    419        if (isAllowed(device, "microphone")) {
    420          microphone = device;
    421          break;
    422        }
    423        if (hasInherentAudioConstraints) {
    424          // Inherent constraints suggest site is looking for a specific mic
    425          break;
    426        }
    427        // Some sites don't look too hard at what they get, and spam gUM without
    428        // adjusting what they ask for to match what they got last time. To keep
    429        // users in charge and reduce prompts, ignore other constraints by
    430        // returning the most-fit microphone a site already has access to.
    431      }
    432      if (!microphone) {
    433        return false;
    434      }
    435    }
    436    let camera;
    437    if (videoInputDevices.length) {
    438      for (let device of videoInputDevices) {
    439        if (isAllowed(device, "camera")) {
    440          camera = device;
    441          break;
    442        }
    443        if (hasInherentVideoConstraints) {
    444          // Inherent constraints suggest site is looking for a specific camera
    445          break;
    446        }
    447        // Some sites don't look too hard at what they get, and spam gUM without
    448        // adjusting what they ask for to match what they got last time. To keep
    449        // users in charge and reduce prompts, ignore other constraints by
    450        // returning the most-fit camera a site already has access to.
    451      }
    452      if (!camera) {
    453        return false;
    454      }
    455    }
    456    let devices = [];
    457    if (camera) {
    458      perms.addFromPrincipal(
    459        aPrincipal,
    460        "MediaManagerVideo",
    461        perms.ALLOW_ACTION,
    462        perms.EXPIRE_SESSION
    463      );
    464      devices.push(camera.deviceIndex);
    465      this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
    466    }
    467    if (microphone) {
    468      devices.push(microphone.deviceIndex);
    469      this.activateDevicePerm(
    470        windowID,
    471        microphone.mediaSource,
    472        microphone.rawId
    473      );
    474    }
    475    this.checkOSPermission(!!camera, !!microphone, false).then(
    476      havePermission => {
    477        if (havePermission) {
    478          this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
    479        } else {
    480          this.denyRequestNoPermission(aRequest);
    481        }
    482      }
    483    );
    484    return true;
    485  }
    486 }
    487 
    488 function prompt(aActor, aBrowser, aRequest) {
    489  let {
    490    audioInputDevices,
    491    videoInputDevices,
    492    audioOutputDevices,
    493    sharingScreen,
    494    sharingAudio,
    495    requestTypes,
    496    isHandlingUserInput,
    497  } = aRequest;
    498 
    499  let principal =
    500    Services.scriptSecurityManager.createContentPrincipalFromOrigin(
    501      aRequest.origin
    502    );
    503 
    504  // For add-on principals, we immediately check for permission instead
    505  // of waiting for the notification to focus. This allows for supporting
    506  // cases such as browserAction popups where no prompt is shown.
    507  if (principal.addonPolicy) {
    508    let isPopup = false;
    509    let isBackground = false;
    510 
    511    for (let view of principal.addonPolicy.extension.views) {
    512      if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
    513        isPopup = true;
    514      }
    515      if (view.viewType == "background" && view.xulBrowser == aBrowser) {
    516        isBackground = true;
    517      }
    518    }
    519 
    520    // Recording from background pages is considered too sensitive and will
    521    // always be denied.
    522    if (isBackground) {
    523      aActor.denyRequest(aRequest);
    524      return;
    525    }
    526 
    527    // If the request comes from a popup, we don't want to show the prompt,
    528    // but we do want to allow the request if the user previously gave permission.
    529    if (isPopup) {
    530      if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
    531        aActor.denyRequest(aRequest);
    532      }
    533      return;
    534    }
    535  }
    536 
    537  // If the request comes from a sidebar,
    538  // the user already gave a persistent permission, skip showing a notification
    539  // otherwise deny request.
    540  if (isSidebar(aBrowser)) {
    541    if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
    542      aActor.denyRequest(aRequest);
    543      return;
    544    }
    545 
    546    return;
    547  }
    548 
    549  // If the user has already denied access once in this tab,
    550  // deny again without even showing the notification icon.
    551  for (const type of requestTypes) {
    552    const permissionID =
    553      type == "AudioCapture" ? "microphone" : type.toLowerCase();
    554    if (
    555      lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
    556        .state == lazy.SitePermissions.BLOCK
    557    ) {
    558      aActor.denyRequest(aRequest);
    559      return;
    560    }
    561  }
    562 
    563  let chromeDoc = aBrowser.ownerDocument;
    564  const localization = new Localization(
    565    ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
    566    true
    567  );
    568 
    569  /** @type {"Screen" | "Camera" | null} */
    570  let reqVideoInput = null;
    571  if (videoInputDevices.length) {
    572    reqVideoInput = sharingScreen ? "Screen" : "Camera";
    573  }
    574  /** @type {"AudioCapture" | "Microphone" | null} */
    575  let reqAudioInput = null;
    576  if (audioInputDevices.length) {
    577    reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
    578  }
    579  const reqAudioOutput = !!audioOutputDevices.length;
    580 
    581  const isFile = principal.schemeIs("file");
    582  const stringId = getPromptMessageId(
    583    reqVideoInput,
    584    reqAudioInput,
    585    reqAudioOutput,
    586    !!aRequest.secondOrigin,
    587    isFile
    588  );
    589  let message;
    590  let originToShow;
    591  if (isFile) {
    592    message = localization.formatValueSync(stringId);
    593    originToShow = null;
    594  } else {
    595    message = localization.formatValueSync(stringId, {
    596      origin: "<>",
    597      thirdParty: "{}",
    598    });
    599    originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
    600  }
    601  let notification; // Used by action callbacks.
    602  const actionL10nIds = [{ id: "webrtc-action-allow" }];
    603 
    604  let notificationSilencingEnabled = Services.prefs.getBoolPref(
    605    "privacy.webrtc.allowSilencingNotifications"
    606  );
    607 
    608  const isNotNowLabelEnabled =
    609    reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
    610  let secondaryActions = [];
    611  if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
    612    // We want to free up the checkbox at the bottom of the permission
    613    // panel for the notification silencing option, so we use a
    614    // different configuration for the permissions panel when
    615    // notification silencing is enabled.
    616 
    617    let permissionName = reqAudioOutput ? "speaker" : "screen";
    618    // When selecting speakers, we always offer 'Not now' instead of 'Block'.
    619    // When selecting screens, we offer 'Not now' if and only if we have a
    620    // (temporary) allow permission for some mic/cam device.
    621    const id = isNotNowLabelEnabled
    622      ? "webrtc-action-not-now"
    623      : "webrtc-action-block";
    624    actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
    625    secondaryActions = [
    626      {
    627        callback() {
    628          aActor.denyRequest(aRequest);
    629          if (!isNotNowLabelEnabled) {
    630            lazy.SitePermissions.setForPrincipal(
    631              principal,
    632              permissionName,
    633              lazy.SitePermissions.BLOCK,
    634              lazy.SitePermissions.SCOPE_TEMPORARY,
    635              notification.browser
    636            );
    637          }
    638        },
    639      },
    640      {
    641        callback() {
    642          aActor.denyRequest(aRequest);
    643          lazy.SitePermissions.setForPrincipal(
    644            principal,
    645            permissionName,
    646            lazy.SitePermissions.BLOCK,
    647            lazy.SitePermissions.SCOPE_PERSISTENT,
    648            notification.browser
    649          );
    650        },
    651      },
    652    ];
    653  } else {
    654    // We have a (temporary) allow permission for some device
    655    // hence we offer a 'Not now' label instead of 'Block'.
    656    const id = isNotNowLabelEnabled
    657      ? "webrtc-action-not-now"
    658      : "webrtc-action-block";
    659    actionL10nIds.push({ id });
    660    secondaryActions = [
    661      {
    662        callback(aState) {
    663          aActor.denyRequest(aRequest);
    664 
    665          const isPersistent = aState?.checkboxChecked;
    666 
    667          // Choosing 'Not now' will not set a block permission
    668          // we just deny the request. This enables certain use cases
    669          // where sites want to switch devices, but users back out of the permission request
    670          // (See Bug 1609578).
    671          // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
    672          if (!isPersistent && isNotNowLabelEnabled) {
    673            return;
    674          }
    675 
    676          // Denying a camera / microphone prompt means we set a temporary or
    677          // persistent permission block. There may still be active grace period
    678          // permissions at this point. We need to remove them.
    679          clearTemporaryGrants(
    680            notification.browser,
    681            reqVideoInput === "Camera",
    682            !!reqAudioInput
    683          );
    684 
    685          const scope = isPersistent
    686            ? lazy.SitePermissions.SCOPE_PERSISTENT
    687            : lazy.SitePermissions.SCOPE_TEMPORARY;
    688          if (reqAudioInput) {
    689            if (!isPersistent) {
    690              // After a temporary block, having permissions.query() calls
    691              // persistently report "granted" would be misleading
    692              maybeClearAlwaysAsk(
    693                principal,
    694                "microphone",
    695                notification.browser
    696              );
    697            }
    698            lazy.SitePermissions.setForPrincipal(
    699              principal,
    700              "microphone",
    701              lazy.SitePermissions.BLOCK,
    702              scope,
    703              notification.browser
    704            );
    705          }
    706          if (reqVideoInput) {
    707            if (!isPersistent && !sharingScreen) {
    708              // After a temporary block, having permissions.query() calls
    709              // persistently report "granted" would be misleading
    710              maybeClearAlwaysAsk(principal, "camera", notification.browser);
    711            }
    712            lazy.SitePermissions.setForPrincipal(
    713              principal,
    714              sharingScreen ? "screen" : "camera",
    715              lazy.SitePermissions.BLOCK,
    716              scope,
    717              notification.browser
    718            );
    719          }
    720        },
    721      },
    722    ];
    723  }
    724 
    725  // The formatMessagesSync method returns an array of results
    726  // for each message that was requested, and for the ones with
    727  // attributes, returns an attributes array with objects like:
    728  //     { name: "label", value: "somevalue" }
    729  const [mainMessage, ...secondaryMessages] = localization
    730    .formatMessagesSync(actionL10nIds)
    731    .map(msg =>
    732      msg.attributes.reduce(
    733        (acc, { name, value }) => ({ ...acc, [name]: value }),
    734        {}
    735      )
    736    );
    737 
    738  const mainAction = {
    739    label: mainMessage.label,
    740    accessKey: mainMessage.accesskey,
    741    // The real callback will be set during the "showing" event. The
    742    // empty function here is so that PopupNotifications.show doesn't
    743    // reject the action.
    744    callback() {},
    745  };
    746 
    747  for (let i = 0; i < secondaryActions.length; ++i) {
    748    secondaryActions[i].label = secondaryMessages[i].label;
    749    secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
    750  }
    751 
    752  let options = {
    753    name: originToShow,
    754    persistent: true,
    755    hideClose: true,
    756    eventCallback(aTopic, aNewBrowser, isCancel) {
    757      if (aTopic == "swapping") {
    758        return true;
    759      }
    760 
    761      let doc = this.browser.ownerDocument;
    762 
    763      // Clean-up video streams of screensharing and camera previews.
    764      if (
    765        (reqVideoInput != "Screen" && reqVideoInput != "Camera") ||
    766        aTopic == "dismissed" ||
    767        aTopic == "removed"
    768      ) {
    769        // Hide the webrtc preview section.
    770        let webrtcPreviewSection = doc.getElementById("webRTC-preview-section");
    771        webrtcPreviewSection.hidden = true;
    772 
    773        // Remove event listeners for camera and window selection menus.
    774        for (const id of [
    775          "webRTC-selectWindow-menupopup",
    776          "webRTC-selectCamera-menupopup",
    777        ]) {
    778          let menuPopup = doc.getElementById(id);
    779          if (menuPopup?._commandEventListener) {
    780            menuPopup.removeEventListener(
    781              "command",
    782              menuPopup._commandEventListener
    783            );
    784            menuPopup._commandEventListener = null;
    785          }
    786        }
    787      }
    788 
    789      if (aTopic == "removed" && notification && isCancel) {
    790        // The notification has been cancelled (e.g. due to entering
    791        // full-screen).  Also cancel the webRTC request.
    792        aActor.denyRequest(aRequest);
    793      } else if (
    794        aTopic == "shown" &&
    795        !notification.wasDismissed &&
    796        reqAudioOutput
    797      ) {
    798        let focusElement =
    799          audioOutputDevices.length > 1
    800            ? doc.getElementById("webRTC-selectSpeaker-richlistbox") // Focus the list on first show so that arrow keys select the speaker.
    801            : doc.querySelector("button.popup-notification-primary-button"); // Or if the list is hidden (only 1 device), focus the primary button.
    802        focusElement.focus();
    803      }
    804 
    805      const isRequestingCamera = reqVideoInput === "Camera";
    806 
    807      if (aTopic == "shown") {
    808        if (!notification.wasDismissed && isRequestingCamera) {
    809          onCameraPromptShown(doc, isHandlingUserInput);
    810        }
    811      }
    812 
    813      if (aTopic != "showing") {
    814        return false;
    815      }
    816 
    817      // If BLOCK has been set persistently in the permission manager or has
    818      // been set on the tab, then it is handled synchronously before we add
    819      // the notification.
    820      // Handling of ALLOW is delayed until the popupshowing event,
    821      // to avoid granting permissions automatically to background tabs.
    822      if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
    823        this.remove();
    824        return true;
    825      }
    826 
    827      /**
    828       * Prepare the device selector for one kind of device.
    829       *
    830       * @param {object[]} devices - available devices of this kind.
    831       * @param {string} IDPrefix - indicating kind of device and so
    832       *   associated UI elements.
    833       * @param {string[]} describedByIDs - an array to which might be
    834       *   appended ids of elements that describe the panel, for the caller to
    835       *   use in the aria-describedby attribute.
    836       */
    837      function listDevices(devices, IDPrefix, describedByIDs) {
    838        let labelID = `${IDPrefix}-single-device-label`;
    839        let list;
    840        let itemParent;
    841        if (IDPrefix == "webRTC-selectSpeaker") {
    842          list = doc.getElementById(`${IDPrefix}-richlistbox`);
    843          itemParent = list;
    844        } else {
    845          itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
    846          list = itemParent.parentNode; // menulist
    847        }
    848        while (itemParent.lastChild) {
    849          itemParent.removeChild(itemParent.lastChild);
    850        }
    851 
    852        // Removing the child nodes of a menupopup doesn't clear the value
    853        // attribute of its menulist. Similary for richlistbox state. This can
    854        // have unfortunate side effects when the list is rebuilt with a
    855        // different content, so we set the selectedIndex explicitly to reset
    856        // state.
    857        let defaultIndex = 0;
    858 
    859        for (let device of devices) {
    860          let item = addDeviceToList(list, device.name, device.deviceIndex);
    861          // Add deviceId property for camera devices so that we can preview them on selection.
    862          if (IDPrefix == "webRTC-selectCamera") {
    863            item.deviceId = device.rawId;
    864          }
    865          if (IDPrefix == "webRTC-selectSpeaker") {
    866            item.addEventListener("dblclick", event => {
    867              // Allow the chosen speakers via
    868              // .popup-notification-primary-button so that
    869              // "security.notification_enable_delay" is checked.
    870              event.target.closest("popupnotification").button.doCommand();
    871            });
    872            if (device.id == aRequest.audioOutputId) {
    873              defaultIndex = device.deviceIndex;
    874            }
    875          }
    876        }
    877        list.selectedIndex = defaultIndex;
    878 
    879        let label = doc.getElementById(labelID);
    880        if (devices.length == 1) {
    881          describedByIDs.push(`${IDPrefix}-icon`, labelID);
    882          label.value = devices[0].name;
    883          label.hidden = false;
    884          list.hidden = true;
    885        } else {
    886          label.hidden = true;
    887          list.hidden = false;
    888        }
    889      }
    890 
    891      let notificationElement = doc.getElementById(
    892        "webRTC-shareDevices-notification"
    893      );
    894 
    895      function checkDisabledWindowMenuItem() {
    896        let list = doc.getElementById("webRTC-selectWindow-menulist");
    897        let item = list.selectedItem;
    898        if (!item || item.hasAttribute("disabled")) {
    899          notificationElement.setAttribute("invalidselection", "true");
    900        } else {
    901          notificationElement.removeAttribute("invalidselection");
    902        }
    903      }
    904 
    905      function listScreenShareDevices(menupopup, devices) {
    906        while (menupopup.lastChild) {
    907          menupopup.removeChild(menupopup.lastChild);
    908        }
    909 
    910        // Removing the child nodes of the menupopup doesn't clear the value
    911        // attribute of the menulist. This can have unfortunate side effects
    912        // when the list is rebuilt with a different content, so we remove
    913        // the value attribute and unset the selectedItem explicitly.
    914        menupopup.parentNode.removeAttribute("value");
    915        menupopup.parentNode.selectedItem = null;
    916 
    917        // "Select a Window or Screen" is the default because we can't and don't
    918        // want to pick a 'default' window to share (Full screen is "scary").
    919        addDeviceToList(
    920          menupopup.parentNode,
    921          localization.formatValueSync("webrtc-pick-window-or-screen"),
    922          "-1"
    923        );
    924        menupopup.appendChild(doc.createXULElement("menuseparator"));
    925 
    926        let isPipeWireDetected = false;
    927 
    928        // Build the list of 'devices'.
    929        let monitorIndex = 1;
    930        for (let i = 0; i < devices.length; ++i) {
    931          let device = devices[i];
    932          let type = device.mediaSource;
    933          let name;
    934          if (device.canRequestOsLevelPrompt) {
    935            // When we share content by PipeWire add only one item to the device
    936            // list. When it's selected PipeWire portal dialog is opened and
    937            // user confirms actual window/screen sharing there.
    938            // Don't mark it as scary as there's an extra confirmation step by
    939            // PipeWire portal dialog.
    940 
    941            isPipeWireDetected = true;
    942            let item = addDeviceToList(
    943              menupopup.parentNode,
    944              localization.formatValueSync("webrtc-share-pipe-wire-portal"),
    945              i,
    946              type
    947            );
    948            item.deviceId = device.rawId;
    949            item.mediaSource = type;
    950 
    951            // In this case the OS sharing dialog will be the only option and
    952            // can be safely pre-selected.
    953            menupopup.parentNode.selectedItem = item;
    954            continue;
    955          } else if (type == "screen") {
    956            // Building screen list from available screens.
    957            if (device.name == "Primary Monitor") {
    958              name = localization.formatValueSync("webrtc-share-entire-screen");
    959            } else {
    960              name = localization.formatValueSync("webrtc-share-monitor", {
    961                monitorIndex,
    962              });
    963              ++monitorIndex;
    964            }
    965          } else {
    966            name = device.name;
    967 
    968            if (type == "application") {
    969              // The application names returned by the platform are of the form:
    970              // <window count>\x1e<application name>
    971              const [count, appName] = name.split("\x1e");
    972              name = localization.formatValueSync("webrtc-share-application", {
    973                appName,
    974                windowCount: parseInt(count),
    975              });
    976            }
    977          }
    978          let item = addDeviceToList(menupopup.parentNode, name, i, type);
    979          item.deviceId = device.rawId;
    980          item.mediaSource = type;
    981          if (device.scary) {
    982            item.scary = true;
    983          }
    984        }
    985 
    986        // Always re-select the "No <type>" item.
    987        doc
    988          .getElementById("webRTC-selectWindow-menulist")
    989          .removeAttribute("value");
    990        doc.getElementById("webRTC-all-windows-shared").hidden = true;
    991 
    992        const webrtcPreview = getOrCreateWebRTCPreviewEl(doc);
    993        webrtcPreview.showPreviewControlButtons = false;
    994 
    995        menupopup._commandEventListener = event => {
    996          checkDisabledWindowMenuItem();
    997 
    998          const { deviceId, mediaSource, scary } = event.target;
    999          if (deviceId == undefined) {
   1000            webrtcPreviewSection.hidden = true;
   1001            return;
   1002          }
   1003          // We have a valid device
   1004          webrtcPreviewSection.hidden = false;
   1005 
   1006          let warning = doc.getElementById("webRTC-previewWarning");
   1007          let warningBox = doc.getElementById("webRTC-previewWarningBox");
   1008          warningBox.hidden = !scary;
   1009          if (scary) {
   1010            const warnId =
   1011              mediaSource == "screen"
   1012                ? "webrtc-share-screen-warning"
   1013                : "webrtc-share-browser-warning";
   1014            doc.l10n.setAttributes(warning, warnId);
   1015 
   1016            // On Catalina, we don't want to blow our chance to show the
   1017            // OS-level helper prompt to enable screen recording if the user
   1018            // intends to reject anyway. OTOH showing it when they click Allow
   1019            // is too late. A happy middle is to show it when the user makes a
   1020            // choice in the picker. This already happens implicitly if the
   1021            // user chooses "Entire desktop", as a side-effect of our preview,
   1022            // we just need to also do it if they choose "Firefox". These are
   1023            // the lone two options when permission is absent on Catalina.
   1024            // Ironically, these are the two sources marked "scary" from a
   1025            // web-sharing perspective, which is why this code resides here.
   1026            // A restart doesn't appear to be necessary in spite of OS wording.
   1027            let scrStatus = {};
   1028            lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
   1029            if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
   1030              lazy.OSPermissions.maybeRequestScreenCapturePermission();
   1031            }
   1032          }
   1033 
   1034          let perms = Services.perms;
   1035          let chromePrincipal =
   1036            Services.scriptSecurityManager.getSystemPrincipal();
   1037          perms.addFromPrincipal(
   1038            chromePrincipal,
   1039            "MediaManagerVideo",
   1040            perms.ALLOW_ACTION,
   1041            perms.EXPIRE_SESSION
   1042          );
   1043 
   1044          // We don't have access to any screen content besides our browser tabs
   1045          // on Wayland, therefore there are no previews we can show.
   1046          if (
   1047            (!isPipeWireDetected || mediaSource == "browser") &&
   1048            Services.prefs.getBoolPref(
   1049              "media.getdisplaymedia.previews.enabled",
   1050              true
   1051            )
   1052          ) {
   1053            webrtcPreview.startPreview({
   1054              deviceId,
   1055              mediaSource,
   1056              showPreviewControlButtons: false,
   1057            });
   1058          }
   1059        };
   1060        menupopup.addEventListener("command", menupopup._commandEventListener);
   1061      }
   1062 
   1063      function addDeviceToList(list, deviceName, deviceIndex, type) {
   1064        let item = list.appendItem(deviceName, deviceIndex);
   1065        item.setAttribute("tooltiptext", deviceName);
   1066        if (type) {
   1067          item.setAttribute("devicetype", type);
   1068        }
   1069 
   1070        if (deviceIndex == "-1") {
   1071          item.setAttribute("disabled", true);
   1072        }
   1073 
   1074        return item;
   1075      }
   1076 
   1077      doc.getElementById("webRTC-selectCamera").hidden = !isRequestingCamera;
   1078      doc.getElementById("webRTC-selectWindowOrScreen").hidden =
   1079        reqVideoInput !== "Screen";
   1080      doc.getElementById("webRTC-selectMicrophone").hidden =
   1081        reqAudioInput !== "Microphone";
   1082      doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
   1083 
   1084      let describedByIDs = ["webRTC-shareDevices-notification-description"];
   1085 
   1086      // Hide the webrtc preview section.
   1087      let webrtcPreviewSection = doc.getElementById("webRTC-preview-section");
   1088      webrtcPreviewSection.hidden = true;
   1089 
   1090      if (sharingScreen) {
   1091        let windowMenupopup = doc.getElementById(
   1092          "webRTC-selectWindow-menupopup"
   1093        );
   1094        listScreenShareDevices(windowMenupopup, videoInputDevices);
   1095        checkDisabledWindowMenuItem();
   1096      } else {
   1097        notificationElement.removeAttribute("invalidselection");
   1098 
   1099        // Make sure the screen share warning is hidden before showing the permission panel.
   1100        let warningBox = doc.getElementById("webRTC-previewWarningBox");
   1101        warningBox.hidden = true;
   1102 
   1103        if (isRequestingCamera) {
   1104          listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
   1105 
   1106          let cameraMenuPopup = doc.getElementById(
   1107            "webRTC-selectCamera-menupopup"
   1108          );
   1109 
   1110          // Set up initial camera preview.
   1111          let webrtcPreview = getOrCreateWebRTCPreviewEl(doc);
   1112          webrtcPreview.showPreviewControlButtons = true;
   1113          // Apply the initial selection.
   1114          webrtcPreview.deviceId =
   1115            cameraMenuPopup.querySelector("[selected]")?.deviceId;
   1116          webrtcPreview.mediaSource = "camera";
   1117 
   1118          // Show the preview section.
   1119          webrtcPreviewSection.hidden = false;
   1120 
   1121          if (cameraMenuPopup) {
   1122            cameraMenuPopup._commandEventListener = event => {
   1123              let { deviceId } = event.target;
   1124              if (deviceId == undefined) {
   1125                webrtcPreviewSection.hidden = true;
   1126                return;
   1127              }
   1128              // Start preview on selection change.
   1129              webrtcPreview.startPreview({ deviceId, mediaSource: "camera" });
   1130            };
   1131            cameraMenuPopup.addEventListener(
   1132              "command",
   1133              cameraMenuPopup._commandEventListener
   1134            );
   1135          }
   1136        }
   1137      }
   1138      if (!sharingAudio) {
   1139        listDevices(
   1140          audioInputDevices,
   1141          "webRTC-selectMicrophone",
   1142          describedByIDs
   1143        );
   1144      }
   1145      listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
   1146 
   1147      // PopupNotifications knows to clear the aria-describedby attribute
   1148      // when hiding, so we don't have to worry about cleaning it up ourselves.
   1149      chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
   1150        "aria-describedby",
   1151        describedByIDs.join(" ")
   1152      );
   1153 
   1154      this.mainAction.callback = async function (aState) {
   1155        let remember = false;
   1156        let silenceNotifications = false;
   1157 
   1158        if (notificationSilencingEnabled && sharingScreen) {
   1159          silenceNotifications = aState && aState.checkboxChecked;
   1160        } else {
   1161          remember = aState && aState.checkboxChecked;
   1162        }
   1163 
   1164        let allowedDevices = [];
   1165        let perms = Services.perms;
   1166        if (reqVideoInput) {
   1167          let listId = sharingScreen
   1168            ? "webRTC-selectWindow-menulist"
   1169            : "webRTC-selectCamera-menulist";
   1170          let videoDeviceIndex = doc.getElementById(listId).value;
   1171          let allowVideoDevice = videoDeviceIndex != "-1";
   1172          if (allowVideoDevice) {
   1173            allowedDevices.push(videoDeviceIndex);
   1174            // Session permission will be removed after use
   1175            // (it's really one-shot, not for the entire session)
   1176            perms.addFromPrincipal(
   1177              principal,
   1178              "MediaManagerVideo",
   1179              perms.ALLOW_ACTION,
   1180              perms.EXPIRE_SESSION
   1181            );
   1182            let { mediaSource, rawId } = videoInputDevices.find(
   1183              ({ deviceIndex }) => deviceIndex == videoDeviceIndex
   1184            );
   1185            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
   1186            if (!sharingScreen) {
   1187              persistGrantOrPromptPermission(principal, "camera", remember);
   1188            }
   1189          }
   1190        }
   1191 
   1192        if (reqAudioInput === "Microphone") {
   1193          let audioDeviceIndex = doc.getElementById(
   1194            "webRTC-selectMicrophone-menulist"
   1195          ).value;
   1196          let allowMic = audioDeviceIndex != "-1";
   1197          if (allowMic) {
   1198            allowedDevices.push(audioDeviceIndex);
   1199            let { mediaSource, rawId } = audioInputDevices.find(
   1200              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
   1201            );
   1202            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
   1203            persistGrantOrPromptPermission(principal, "microphone", remember);
   1204          }
   1205        } else if (reqAudioInput === "AudioCapture") {
   1206          // Only one device possible for audio capture.
   1207          allowedDevices.push(0);
   1208        }
   1209 
   1210        if (reqAudioOutput) {
   1211          let audioDeviceIndex = doc.getElementById(
   1212            "webRTC-selectSpeaker-richlistbox"
   1213          ).value;
   1214          let allowSpeaker = audioDeviceIndex != "-1";
   1215          if (allowSpeaker) {
   1216            allowedDevices.push(audioDeviceIndex);
   1217            let { id } = audioOutputDevices.find(
   1218              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
   1219            );
   1220            lazy.SitePermissions.setForPrincipal(
   1221              principal,
   1222              ["speaker", id].join("^"),
   1223              lazy.SitePermissions.ALLOW
   1224            );
   1225          }
   1226        }
   1227 
   1228        if (!allowedDevices.length) {
   1229          aActor.denyRequest(aRequest);
   1230          return;
   1231        }
   1232 
   1233        const camNeeded = reqVideoInput === "Camera";
   1234        const micNeeded = !!reqAudioInput;
   1235        const scrNeeded = reqVideoInput === "Screen";
   1236        const havePermission = await aActor.checkOSPermission(
   1237          camNeeded,
   1238          micNeeded,
   1239          scrNeeded
   1240        );
   1241        if (!havePermission) {
   1242          aActor.denyRequestNoPermission(aRequest);
   1243          return;
   1244        }
   1245 
   1246        aActor.sendAsyncMessage("webrtc:Allow", {
   1247          callID: aRequest.callID,
   1248          windowID: aRequest.windowID,
   1249          devices: allowedDevices,
   1250          suppressNotifications: silenceNotifications,
   1251        });
   1252      };
   1253 
   1254      // If we haven't handled the permission yet, we want to show the doorhanger.
   1255      return false;
   1256    },
   1257    queue: true,
   1258  };
   1259 
   1260  function shouldShowAlwaysRemember() {
   1261    // Don't offer "always remember" action in PB mode
   1262    if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
   1263      return false;
   1264    }
   1265 
   1266    // Don't offer "always remember" action in maybe unsafe permission
   1267    // delegation
   1268    if (aRequest.secondOrigin) {
   1269      return false;
   1270    }
   1271 
   1272    // Speaker grants are always remembered, so no checkbox is required.
   1273    if (reqAudioOutput) {
   1274      return false;
   1275    }
   1276 
   1277    return true;
   1278  }
   1279 
   1280  function getRememberCheckboxLabel() {
   1281    if (reqVideoInput == "Camera") {
   1282      if (reqAudioInput == "Microphone") {
   1283        return "webrtc-remember-allow-checkbox-camera-and-microphone";
   1284      }
   1285      return "webrtc-remember-allow-checkbox-camera";
   1286    }
   1287 
   1288    if (reqAudioInput == "Microphone") {
   1289      return "webrtc-remember-allow-checkbox-microphone";
   1290    }
   1291 
   1292    return "webrtc-remember-allow-checkbox";
   1293  }
   1294 
   1295  if (shouldShowAlwaysRemember()) {
   1296    // Disable the permanent 'Allow' action if the connection isn't secure, or for
   1297    // screen/audio sharing (because we can't guess which window the user wants to
   1298    // share without prompting). Note that we never enter this block for private
   1299    // browsing windows.
   1300    let reason = "";
   1301    if (sharingScreen) {
   1302      reason = "webrtc-reason-for-no-permanent-allow-screen";
   1303    } else if (sharingAudio) {
   1304      reason = "webrtc-reason-for-no-permanent-allow-audio";
   1305    } else if (!aRequest.secure) {
   1306      reason = "webrtc-reason-for-no-permanent-allow-insecure";
   1307    }
   1308 
   1309    options.checkbox = {
   1310      label: localization.formatValueSync(getRememberCheckboxLabel()),
   1311      checked: principal.isAddonOrExpandedAddonPrincipal,
   1312      checkedState: reason
   1313        ? {
   1314            disableMainAction: true,
   1315            warningLabel: localization.formatValueSync(reason),
   1316          }
   1317        : undefined,
   1318    };
   1319  }
   1320 
   1321  // If the notification silencing feature is enabled and we're sharing a
   1322  // screen, then the checkbox for the permission panel is what controls
   1323  // notification silencing.
   1324  if (notificationSilencingEnabled && sharingScreen) {
   1325    options.checkbox = {
   1326      label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
   1327      checked: false,
   1328      checkedState: {
   1329        disableMainAction: false,
   1330      },
   1331    };
   1332  }
   1333 
   1334  let anchorId = "webRTC-shareDevices-notification-icon";
   1335  if (reqVideoInput === "Screen") {
   1336    anchorId = "webRTC-shareScreen-notification-icon";
   1337  } else if (!reqVideoInput) {
   1338    if (reqAudioInput && !reqAudioOutput) {
   1339      anchorId = "webRTC-shareMicrophone-notification-icon";
   1340    } else if (!reqAudioInput && reqAudioOutput) {
   1341      anchorId = "webRTC-shareSpeaker-notification-icon";
   1342    }
   1343  }
   1344 
   1345  if (aRequest.secondOrigin) {
   1346    options.secondName = lazy.webrtcUI.getHostOrExtensionName(
   1347      null,
   1348      aRequest.secondOrigin
   1349    );
   1350  }
   1351 
   1352  notification = chromeDoc.defaultView.PopupNotifications.show(
   1353    aBrowser,
   1354    "webRTC-shareDevices",
   1355    message,
   1356    anchorId,
   1357    mainAction,
   1358    secondaryActions,
   1359    options
   1360  );
   1361  notification.callID = aRequest.callID;
   1362 }
   1363 
   1364 /**
   1365 * @param {"Screen" | "Camera" | null} reqVideoInput
   1366 * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
   1367 * @param {boolean} reqAudioOutput
   1368 * @param {boolean} delegation - Is the access delegated to a third party?
   1369 * @param {boolean} isFile - Is the request coming from a file?
   1370 * @returns {string} Localization message identifier
   1371 */
   1372 function getPromptMessageId(
   1373  reqVideoInput,
   1374  reqAudioInput,
   1375  reqAudioOutput,
   1376  delegation,
   1377  isFile
   1378 ) {
   1379  switch (reqVideoInput) {
   1380    case "Camera":
   1381      switch (reqAudioInput) {
   1382        case "Microphone":
   1383          if (isFile) {
   1384            return "webrtc-allow-share-camera-and-microphone-with-file";
   1385          }
   1386          if (delegation) {
   1387            return "webrtc-allow-share-camera-and-microphone-unsafe-delegation";
   1388          }
   1389          return "webrtc-allow-share-camera-and-microphone";
   1390        case "AudioCapture":
   1391          if (isFile) {
   1392            return "webrtc-allow-share-camera-and-audio-capture-with-file";
   1393          }
   1394          if (delegation) {
   1395            return "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation";
   1396          }
   1397          return "webrtc-allow-share-camera-and-audio-capture";
   1398        default:
   1399          if (isFile) {
   1400            return "webrtc-allow-share-camera-with-file";
   1401          }
   1402          if (delegation) {
   1403            return "webrtc-allow-share-camera-unsafe-delegation";
   1404          }
   1405          return "webrtc-allow-share-camera";
   1406      }
   1407 
   1408    case "Screen":
   1409      switch (reqAudioInput) {
   1410        case "Microphone":
   1411          if (isFile) {
   1412            return "webrtc-allow-share-screen-and-microphone-with-file";
   1413          }
   1414          if (delegation) {
   1415            return "webrtc-allow-share-screen-and-microphone-unsafe-delegation";
   1416          }
   1417          return "webrtc-allow-share-screen-and-microphone";
   1418        case "AudioCapture":
   1419          if (isFile) {
   1420            return "webrtc-allow-share-screen-and-audio-capture-with-file";
   1421          }
   1422          if (delegation) {
   1423            return "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation";
   1424          }
   1425          return "webrtc-allow-share-screen-and-audio-capture";
   1426        default:
   1427          if (isFile) {
   1428            return "webrtc-allow-share-screen-with-file";
   1429          }
   1430          if (delegation) {
   1431            return "webrtc-allow-share-screen-unsafe-delegation";
   1432          }
   1433          return "webrtc-allow-share-screen";
   1434      }
   1435 
   1436    default:
   1437      switch (reqAudioInput) {
   1438        case "Microphone":
   1439          if (isFile) {
   1440            return "webrtc-allow-share-microphone-with-file";
   1441          }
   1442          if (delegation) {
   1443            return "webrtc-allow-share-microphone-unsafe-delegation";
   1444          }
   1445          return "webrtc-allow-share-microphone";
   1446        case "AudioCapture":
   1447          if (isFile) {
   1448            return "webrtc-allow-share-audio-capture-with-file";
   1449          }
   1450          if (delegation) {
   1451            return "webrtc-allow-share-audio-capture-unsafe-delegation";
   1452          }
   1453          return "webrtc-allow-share-audio-capture";
   1454        default:
   1455          // This should be always true, if we've reached this far.
   1456          if (reqAudioOutput) {
   1457            if (isFile) {
   1458              return "webrtc-allow-share-speaker-with-file";
   1459            }
   1460            if (delegation) {
   1461              return "webrtc-allow-share-speaker-unsafe-delegation";
   1462            }
   1463            return "webrtc-allow-share-speaker";
   1464          }
   1465          return undefined;
   1466      }
   1467  }
   1468 }
   1469 
   1470 /**
   1471 * Checks whether we have a microphone/camera in use by checking the activePerms map
   1472 * or if we have an allow permission for a microphone/camera in sitePermissions
   1473 *
   1474 * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
   1475 * @return true if one of the above conditions is met
   1476 */
   1477 function allowedOrActiveCameraOrMicrophone(browser) {
   1478  // Do we have an allow permission for cam/mic in the permissions manager?
   1479  if (
   1480    lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
   1481      return (
   1482        perm.state == lazy.SitePermissions.ALLOW &&
   1483        (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
   1484      );
   1485    })
   1486  ) {
   1487    // Return early, no need to check for active devices
   1488    return true;
   1489  }
   1490 
   1491  // Do we have an active device?
   1492  return (
   1493    // Find all windowIDs that belong to our browsing contexts
   1494    browser.browsingContext
   1495      .getAllBrowsingContextsInSubtree()
   1496      // Only keep the outerWindowIds
   1497      .map(bc => bc.currentWindowGlobal?.outerWindowId)
   1498      .filter(id => id != null)
   1499      // We have an active device if one of our windowIds has a non empty map in the activePerms map
   1500      // that includes one device of type "camera" or "microphone"
   1501      .some(id => {
   1502        let map = lazy.webrtcUI.activePerms.get(id);
   1503        if (!map) {
   1504          // This windowId has no active device
   1505          return false;
   1506        }
   1507        // Let's see if one of the devices is a camera or a microphone
   1508        let types = [...map.values()];
   1509        return types.includes("microphone") || types.includes("camera");
   1510      })
   1511  );
   1512 }
   1513 
   1514 function removePrompt(aBrowser, aCallId) {
   1515  let chromeWin = aBrowser.ownerGlobal;
   1516  let notification = chromeWin.PopupNotifications.getNotification(
   1517    "webRTC-shareDevices",
   1518    aBrowser
   1519  );
   1520  if (notification && notification.callID == aCallId) {
   1521    notification.remove();
   1522  }
   1523 }
   1524 
   1525 /**
   1526 * Clears temporary permission grants used for WebRTC device grace periods.
   1527 *
   1528 * @param browser - Browser element to clear permissions for.
   1529 * @param {boolean} clearCamera - Clear camera grants.
   1530 * @param {boolean} clearMicrophone - Clear microphone grants.
   1531 */
   1532 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
   1533  if (!clearCamera && !clearMicrophone) {
   1534    // Nothing to clear.
   1535    return;
   1536  }
   1537  let perms = lazy.SitePermissions.getAllForBrowser(browser);
   1538  perms
   1539    .filter(perm => {
   1540      let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
   1541      // We only want to clear WebRTC grace periods. These are temporary, device
   1542      // specifc (double-keyed) microphone or camera permissions.
   1543      return (
   1544        key &&
   1545        perm.state == lazy.SitePermissions.ALLOW &&
   1546        perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
   1547        ((clearCamera && id == "camera") ||
   1548          (clearMicrophone && id == "microphone"))
   1549      );
   1550    })
   1551    .forEach(perm =>
   1552      lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
   1553    );
   1554 }
   1555 
   1556 /**
   1557 * Persist an ALLOW state if the remember option is true.
   1558 * Otherwise, persist PROMPT so that we can later tell the site
   1559 * that permission was granted once before.
   1560 * This makes Firefox seem much more like Chrome to sites that
   1561 * expect a one-off, persistent permission grant for cam/mic.
   1562 *
   1563 * @param principal - Principal to add permission to.
   1564 * @param {string} permissionName - name of permission.
   1565 * @param remember - whether the grant should be persisted.
   1566 */
   1567 function persistGrantOrPromptPermission(principal, permissionName, remember) {
   1568  // There are cases like unsafe delegation where a prompt appears
   1569  // even in ALLOW state, so make sure to not overwrite it (there's
   1570  // no remember checkbox in those cases)
   1571  if (
   1572    lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
   1573    lazy.SitePermissions.ALLOW
   1574  ) {
   1575    return;
   1576  }
   1577 
   1578  lazy.SitePermissions.setForPrincipal(
   1579    principal,
   1580    permissionName,
   1581    remember ? lazy.SitePermissions.ALLOW : lazy.SitePermissions.PROMPT
   1582  );
   1583 }
   1584 
   1585 /**
   1586 * Clears any persisted PROMPT (aka Always Ask) permission.
   1587 *
   1588 * @param principal - Principal to remove permission from.
   1589 * @param {string} permissionName - name of permission.
   1590 * @param browser - Browser element to clear permission for.
   1591 */
   1592 function maybeClearAlwaysAsk(principal, permissionName, browser) {
   1593  // For the "Always Ask" user choice, only persisted PROMPT is used,
   1594  // so no need to scan through temporary permissions.
   1595  if (
   1596    lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
   1597    lazy.SitePermissions.PROMPT
   1598  ) {
   1599    lazy.SitePermissions.removeFromPrincipal(
   1600      principal,
   1601      permissionName,
   1602      browser
   1603    );
   1604  }
   1605 }
   1606 
   1607 /**
   1608 * Helper for lazily creating the webrtc-preview element.
   1609 *
   1610 * @param {Document} chromeDoc - The chrome document to create the webrtc-preview element in.
   1611 * @returns {HTMLElement} The webrtc-preview element which has been inserted into the DOM.
   1612 */
   1613 function getOrCreateWebRTCPreviewEl(chromeDoc) {
   1614  let previewSection = chromeDoc.getElementById("webRTC-preview-section");
   1615  let previewEl = previewSection.querySelector("#webRTC-preview");
   1616  if (!previewEl) {
   1617    previewEl = chromeDoc.createElement("webrtc-preview");
   1618    previewEl.id = "webRTC-preview";
   1619    previewSection.insertBefore(previewEl, previewSection.firstChild);
   1620  }
   1621  return previewEl;
   1622 }
   1623 
   1624 /**
   1625 * On prompt "shown", if a camera permission request was made as the result of
   1626 * user interaction start the camera preview automatically.
   1627 * While websites don't have access to the camera preview, giving websites the
   1628 * ability to turn on the users camera without any user interaction can be
   1629 * scary. If there is no user input we offer the user to start the preview
   1630 * manually.
   1631 *
   1632 * @param {Document} doc - The chrome document containing the prompt.
   1633 * @param {boolean} isHandlingUserInput - Whether the prompt is shown as a
   1634 * result of user interaction.
   1635 */
   1636 function onCameraPromptShown(doc, isHandlingUserInput) {
   1637  // Skip if the request was made without user input.
   1638  if (!isHandlingUserInput) {
   1639    return;
   1640  }
   1641 
   1642  // Skip if the entire preview section is hidden.
   1643  if (doc.getElementById("webRTC-preview-section").hidden) {
   1644    return;
   1645  }
   1646 
   1647  // Skip if no device is selected.
   1648  let cameraMenuPopup = doc.getElementById("webRTC-selectCamera-menupopup");
   1649  let deviceId = cameraMenuPopup?.querySelector("[selected]")?.deviceId;
   1650  if (!deviceId) {
   1651    return;
   1652  }
   1653 
   1654  let webrtcPreview = doc.getElementById("webRTC-preview");
   1655  // Pass deviceId and mediaSource to make sure they're up to date,
   1656  // matching the user selection.
   1657  webrtcPreview?.startPreview({ deviceId, mediaSource: "camera" });
   1658 }
   1659 
   1660 function isSidebar(browser) {
   1661  const sidebarBrowser =
   1662    browser.browsingContext?.topChromeWindow?.SidebarController?.browser;
   1663  if (!sidebarBrowser) {
   1664    return false;
   1665  }
   1666 
   1667  const nestedBrowsers =
   1668    sidebarBrowser.contentDocument.querySelectorAll("browser");
   1669  return Array.from(nestedBrowsers).some(b => b === browser);
   1670 }