Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  WebRTCParent.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
  webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "OSPermissions",
  "@mozilla.org/ospermissionrequest;1",
  "nsIOSPermissionRequest"
);

export class WebRTCParent extends JSWindowActorParent {
  didDestroy() {
    // Media stream tracks end on unload, so call stopRecording() on them early
    // *before* we go away, to ensure we're working with the right principal.
    this.stopRecording(this.manager.outerWindowId);
    lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
    // Must clear activePerms here to prevent them from being read by laggard
    // stopRecording() calls, which due to IPC, may come in *after* navigation.
    // This is to prevent granting temporary grace periods to the wrong page.
    lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
  }

  getBrowser() {
    return this.browsingContext.top.embedderElement;
  }

  receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "rtcpeer:Request": {
        let params = Object.freeze(
          Object.assign(
            {
              origin: this.manager.documentPrincipal.origin,
            },
            aMessage.data
          )
        );

        let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);

        (async function () {
          for (let blocker of blockers) {
            try {
              let result = await blocker(params);
              if (result == "deny") {
                return false;
              }
            } catch (err) {
              console.error(`error in PeerConnection blocker: ${err.message}`);
            }
          }
          return true;
        })().then(decision => {
          let message;
          if (decision) {
            lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
            message = "rtcpeer:Allow";
          } else {
            lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
            message = "rtcpeer:Deny";
          }

          this.sendAsyncMessage(message, {
            callID: params.callID,
            windowID: params.windowID,
          });
        });
        break;
      }
      case "rtcpeer:CancelRequest": {
        let params = Object.freeze({
          origin: this.manager.documentPrincipal.origin,
          callID: aMessage.data,
        });
        lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
        break;
      }
      case "webrtc:Request": {
        let data = aMessage.data;

        // Record third party origins for telemetry.
        let isThirdPartyOrigin =
          this.manager.documentPrincipal.origin !=
          this.manager.topWindowContext.documentPrincipal.origin;
        data.isThirdPartyOrigin = isThirdPartyOrigin;

        data.origin = this.manager.topWindowContext.documentPrincipal.origin;

        let browser = this.getBrowser();
        if (browser.fxrPermissionPrompt) {
          // For Firefox Reality on Desktop, switch to a different mechanism to
          // prompt the user since fewer permissions are available and since many
          // UI dependencies are not available.
          browser.fxrPermissionPrompt(data);
        } else {
          prompt(this, this.getBrowser(), data);
        }
        break;
      }
      case "webrtc:StopRecording":
        this.stopRecording(
          aMessage.data.windowID,
          aMessage.data.mediaSource,
          aMessage.data.rawID
        );
        break;
      case "webrtc:CancelRequest": {
        let browser = this.getBrowser();
        // browser can be null when closing the window
        if (browser) {
          removePrompt(browser, aMessage.data);
        }
        break;
      }
      case "webrtc:UpdateIndicators": {
        let { data } = aMessage;
        data.documentURI = this.manager.documentURI?.spec;
        if (data.windowId) {
          if (!data.remove) {
            data.principal = this.manager.topWindowContext.documentPrincipal;
          }
          lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
        }
        this.updateIndicators(data);
        break;
      }
    }
  }

  updateIndicators(aData) {
    let browsingContext = this.browsingContext;
    let state = lazy.webrtcUI.updateIndicators(browsingContext.top);

    let browser = this.getBrowser();
    if (!browser) {
      return;
    }

    state.browsingContext = browsingContext;
    state.windowId = aData.windowId;

    let tabbrowser = browser.ownerGlobal.gBrowser;
    if (tabbrowser) {
      tabbrowser.updateBrowserSharing(browser, {
        webRTC: state,
      });
    }
  }

  denyRequest(aRequest) {
    this.sendAsyncMessage("webrtc:Deny", {
      callID: aRequest.callID,
      windowID: aRequest.windowID,
    });
  }

  //
  // Deny the request because the browser does not have access to the
  // camera or microphone due to OS security restrictions. The user may
  // have granted camera/microphone access to the site, but not have
  // allowed the browser access in OS settings.
  //
  denyRequestNoPermission(aRequest) {
    this.sendAsyncMessage("webrtc:Deny", {
      callID: aRequest.callID,
      windowID: aRequest.windowID,
      noOSPermission: true,
    });
  }

  //
  // Check if we have permission to access the camera or screen-sharing and/or
  // microphone at the OS level. Triggers a request to access the device if access
  // is needed and the permission state has not yet been determined.
  //
  async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
    // Don't trigger OS permission requests for fake devices. Fake devices don't
    // require OS permission and the dialogs are problematic in automated testing
    // (where fake devices are used) because they require user interaction.
    if (
      !scrNeeded &&
      Services.prefs.getBoolPref("media.navigator.streams.fake", false)
    ) {
      return true;
    }
    let camStatus = {},
      micStatus = {};
    if (camNeeded || micNeeded) {
      lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
    }
    if (camNeeded) {
      let camPermission = camStatus.value;
      let camAccessible = await this.checkAndGetOSPermission(
        camPermission,
        lazy.OSPermissions.requestVideoCapturePermission
      );
      if (!camAccessible) {
        return false;
      }
    }
    if (micNeeded) {
      let micPermission = micStatus.value;
      let micAccessible = await this.checkAndGetOSPermission(
        micPermission,
        lazy.OSPermissions.requestAudioCapturePermission
      );
      if (!micAccessible) {
        return false;
      }
    }
    let scrStatus = {};
    if (scrNeeded) {
      lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
      if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
        lazy.OSPermissions.maybeRequestScreenCapturePermission();
        return false;
      }
    }
    return true;
  }

  //
  // Given a device's permission, return true if the device is accessible. If
  // the device's permission is not yet determined, request access to the device.
  // |requestPermissionFunc| must return a promise that resolves with true
  // if the device is accessible and false otherwise.
  //
  async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
    if (
      devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
      devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
    ) {
      return false;
    }
    if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
      let deviceAllowed = await requestPermissionFunc();
      if (!deviceAllowed) {
        return false;
      }
    }
    return true;
  }

  stopRecording(aOuterWindowId, aMediaSource, aRawId) {
    for (let { browsingContext, state } of lazy.webrtcUI._streams) {
      if (browsingContext == this.browsingContext) {
        let { principal } = state;
        for (let { mediaSource, rawId } of state.devices) {
          if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
            continue;
          }
          // Deactivate this device (no aRawId means all devices).
          this.deactivateDevicePerm(
            aOuterWindowId,
            mediaSource,
            rawId,
            principal
          );
        }
      }
    }
  }

  /**
   * Add a device record to webrtcUI.activePerms, denoting a device as in use.
   * Important to call for permission grace periods to work correctly.
   */
  activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
      lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
    }
    lazy.webrtcUI.activePerms
      .get(this.manager.outerWindowId)
      .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
  }

  /**
   * Remove a device record from webrtcUI.activePerms, denoting a device as
   * no longer in use by the site. Meaning: gUM requests for this device will
   * no longer be implicitly granted through the webrtcUI.activePerms mechanism.
   *
   * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
   * grant is extended for an additional period of time through SitePermissions.
   */
  deactivateDevicePerm(
    aOuterWindowId,
    aMediaSource,
    aId,
    aPermissionPrincipal
  ) {
    // If we don't have active permissions for the given window anymore don't
    // set a grace period. This happens if there has been a user revoke and
    // webrtcUI clears the permissions.
    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
      return;
    }
    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
    map.delete(aOuterWindowId + aMediaSource + aId);

    // Add a permission grace period for camera and microphone only
    if (
      (aMediaSource != "camera" && aMediaSource != "microphone") ||
      !this.browsingContext.top.embedderElement
    ) {
      return;
    }
    let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
    if (gracePeriodMs > 0) {
      // A grace period is extended (even past navigation) to this outer window
      // + origin + deviceId only. This avoids re-prompting without the user
      // having to persist permission to the site, in a common case of a web
      // conference asking them for the camera in a lobby page, before
      // navigating to the actual meeting room page. Does not survive tab close.
      //
      // Caution: since navigation causes deactivation, we may be in the middle
      // of one. We must pass in a principal & URI for SitePermissions to use
      // instead of browser.currentURI, because the latter may point to a new
      // page already, and we must not leak permission to unrelated pages.
      //
      let permissionName = [aMediaSource, aId].join("^");
      lazy.SitePermissions.setForPrincipal(
        aPermissionPrincipal,
        permissionName,
        lazy.SitePermissions.ALLOW,
        lazy.SitePermissions.SCOPE_TEMPORARY,
        this.browsingContext.top.embedderElement,
        gracePeriodMs
      );
    }
  }

  /**
   * Checks if the principal has sufficient permissions
   * to fulfill the given request. If the request can be
   * fulfilled, a message is sent to the child
   * signaling that WebRTC permissions were given and
   * this function will return true.
   */
  checkRequestAllowed(aRequest, aPrincipal) {
    if (!aRequest.secure) {
      return false;
    }
    // Always prompt for screen sharing
    if (aRequest.sharingScreen) {
      return false;
    }
    let {
      callID,
      windowID,
      audioInputDevices,
      videoInputDevices,
      audioOutputDevices,
      hasInherentAudioConstraints,
      hasInherentVideoConstraints,
      audioOutputId,
    } = aRequest;

    if (audioOutputDevices?.length) {
      // Prompt if a specific device is not requested, available and allowed.
      let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
      if (
        !device ||
        !lazy.SitePermissions.getForPrincipal(
          aPrincipal,
          ["speaker", device.id].join("^"),
          this.getBrowser()
        ).state == lazy.SitePermissions.ALLOW
      ) {
        return false;
      }
      this.sendAsyncMessage("webrtc:Allow", {
        callID,
        windowID,
        devices: [device.deviceIndex],
      });
      return true;
    }

    let { perms } = Services;
    if (
      perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
    ) {
      perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
    }

    // Don't use persistent permissions from the top-level principal
    // if we're handling a potentially insecure third party
    // through a wildcard ("*") allow attribute.
    let limited = aRequest.secondOrigin;

    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
    // We consider a camera or mic active if it is active or was active within a
    // grace period of milliseconds ago.
    const isAllowed = ({ mediaSource, rawId }, permissionID) =>
      map?.get(windowID + mediaSource + rawId) ||
      (!limited &&
        (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
          lazy.SitePermissions.ALLOW ||
          lazy.SitePermissions.getForPrincipal(
            aPrincipal,
            [mediaSource, rawId].join("^"),
            this.getBrowser()
          ).state == lazy.SitePermissions.ALLOW));

    let microphone;
    if (audioInputDevices.length) {
      for (let device of audioInputDevices) {
        if (isAllowed(device, "microphone")) {
          microphone = device;
          break;
        }
        if (hasInherentAudioConstraints) {
          // Inherent constraints suggest site is looking for a specific mic
          break;
        }
        // Some sites don't look too hard at what they get, and spam gUM without
        // adjusting what they ask for to match what they got last time. To keep
        // users in charge and reduce prompts, ignore other constraints by
        // returning the most-fit microphone a site already has access to.
      }
      if (!microphone) {
        return false;
      }
    }
    let camera;
    if (videoInputDevices.length) {
      for (let device of videoInputDevices) {
        if (isAllowed(device, "camera")) {
          camera = device;
          break;
        }
        if (hasInherentVideoConstraints) {
          // Inherent constraints suggest site is looking for a specific camera
          break;
        }
        // Some sites don't look too hard at what they get, and spam gUM without
        // adjusting what they ask for to match what they got last time. To keep
        // users in charge and reduce prompts, ignore other constraints by
        // returning the most-fit camera a site already has access to.
      }
      if (!camera) {
        return false;
      }
    }
    let devices = [];
    if (camera) {
      perms.addFromPrincipal(
        aPrincipal,
        "MediaManagerVideo",
        perms.ALLOW_ACTION,
        perms.EXPIRE_SESSION
      );
      devices.push(camera.deviceIndex);
      this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
    }
    if (microphone) {
      devices.push(microphone.deviceIndex);
      this.activateDevicePerm(
        windowID,
        microphone.mediaSource,
        microphone.rawId
      );
    }
    this.checkOSPermission(!!camera, !!microphone, false).then(
      havePermission => {
        if (havePermission) {
          this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
        } else {
          this.denyRequestNoPermission(aRequest);
        }
      }
    );
    return true;
  }
}

function prompt(aActor, aBrowser, aRequest) {
  let {
    audioInputDevices,
    videoInputDevices,
    audioOutputDevices,
    sharingScreen,
    sharingAudio,
    requestTypes,
  } = aRequest;

  let principal =
    Services.scriptSecurityManager.createContentPrincipalFromOrigin(
      aRequest.origin
    );

  // For add-on principals, we immediately check for permission instead
  // of waiting for the notification to focus. This allows for supporting
  // cases such as browserAction popups where no prompt is shown.
  if (principal.addonPolicy) {
    let isPopup = false;
    let isBackground = false;

    for (let view of principal.addonPolicy.extension.views) {
      if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
        isPopup = true;
      }
      if (view.viewType == "background" && view.xulBrowser == aBrowser) {
        isBackground = true;
      }
    }

    // Recording from background pages is considered too sensitive and will
    // always be denied.
    if (isBackground) {
      aActor.denyRequest(aRequest);
      return;
    }

    // If the request comes from a popup, we don't want to show the prompt,
    // but we do want to allow the request if the user previously gave permission.
    if (isPopup) {
      if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
        aActor.denyRequest(aRequest);
      }
      return;
    }
  }

  // If the user has already denied access once in this tab,
  // deny again without even showing the notification icon.
  for (const type of requestTypes) {
    const permissionID =
      type == "AudioCapture" ? "microphone" : type.toLowerCase();
    if (
      lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
        .state == lazy.SitePermissions.BLOCK
    ) {
      aActor.denyRequest(aRequest);
      return;
    }
  }

  let chromeDoc = aBrowser.ownerDocument;
  const localization = new Localization(
    ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
    true
  );

  /** @type {"Screen" | "Camera" | null} */
  let reqVideoInput = null;
  if (videoInputDevices.length) {
    reqVideoInput = sharingScreen ? "Screen" : "Camera";
  }
  /** @type {"AudioCapture" | "Microphone" | null} */
  let reqAudioInput = null;
  if (audioInputDevices.length) {
    reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
  }
  const reqAudioOutput = !!audioOutputDevices.length;

  const stringId = getPromptMessageId(
    reqVideoInput,
    reqAudioInput,
    reqAudioOutput,
    !!aRequest.secondOrigin
  );
  let message;
  let originToShow;
  if (principal.schemeIs("file")) {
    message = localization.formatValueSync(stringId + "-with-file");
    originToShow = null;
  } else {
    message = localization.formatValueSync(stringId, {
      origin: "<>",
      thirdParty: "{}",
    });
    originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
  }
  let notification; // Used by action callbacks.
  const actionL10nIds = [{ id: "webrtc-action-allow" }];

  let notificationSilencingEnabled = Services.prefs.getBoolPref(
    "privacy.webrtc.allowSilencingNotifications"
  );

  const isNotNowLabelEnabled =
    reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
  let secondaryActions = [];
  if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
    // We want to free up the checkbox at the bottom of the permission
    // panel for the notification silencing option, so we use a
    // different configuration for the permissions panel when
    // notification silencing is enabled.

    let permissionName = reqAudioOutput ? "speaker" : "screen";
    // When selecting speakers, we always offer 'Not now' instead of 'Block'.
    // When selecting screens, we offer 'Not now' if and only if we have a
    // (temporary) allow permission for some mic/cam device.
    const id = isNotNowLabelEnabled
      ? "webrtc-action-not-now"
      : "webrtc-action-block";
    actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
    secondaryActions = [
      {
        callback() {
          aActor.denyRequest(aRequest);
          if (!isNotNowLabelEnabled) {
            lazy.SitePermissions.setForPrincipal(
              principal,
              permissionName,
              lazy.SitePermissions.BLOCK,
              lazy.SitePermissions.SCOPE_TEMPORARY,
              notification.browser
            );
          }
        },
      },
      {
        callback() {
          aActor.denyRequest(aRequest);
          lazy.SitePermissions.setForPrincipal(
            principal,
            permissionName,
            lazy.SitePermissions.BLOCK,
            lazy.SitePermissions.SCOPE_PERSISTENT,
            notification.browser
          );
        },
      },
    ];
  } else {
    // We have a (temporary) allow permission for some device
    // hence we offer a 'Not now' label instead of 'Block'.
    const id = isNotNowLabelEnabled
      ? "webrtc-action-not-now"
      : "webrtc-action-block";
    actionL10nIds.push({ id });
    secondaryActions = [
      {
        callback(aState) {
          aActor.denyRequest(aRequest);

          const isPersistent = aState?.checkboxChecked;

          // Choosing 'Not now' will not set a block permission
          // we just deny the request. This enables certain use cases
          // where sites want to switch devices, but users back out of the permission request
          // (See Bug 1609578).
          // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
          if (!isPersistent && isNotNowLabelEnabled) {
            return;
          }

          // Denying a camera / microphone prompt means we set a temporary or
          // persistent permission block. There may still be active grace period
          // permissions at this point. We need to remove them.
          clearTemporaryGrants(
            notification.browser,
            reqVideoInput === "Camera",
            !!reqAudioInput
          );

          const scope = isPersistent
            ? lazy.SitePermissions.SCOPE_PERSISTENT
            : lazy.SitePermissions.SCOPE_TEMPORARY;
          if (reqAudioInput) {
            if (!isPersistent) {
              // After a temporary block, having permissions.query() calls
              // persistently report "granted" would be misleading
              maybeClearAlwaysAsk(
                principal,
                "microphone",
                notification.browser
              );
            }
            lazy.SitePermissions.setForPrincipal(
              principal,
              "microphone",
              lazy.SitePermissions.BLOCK,
              scope,
              notification.browser
            );
          }
          if (reqVideoInput) {
            if (!isPersistent && !sharingScreen) {
              // After a temporary block, having permissions.query() calls
              // persistently report "granted" would be misleading
              maybeClearAlwaysAsk(principal, "camera", notification.browser);
            }
            lazy.SitePermissions.setForPrincipal(
              principal,
              sharingScreen ? "screen" : "camera",
              lazy.SitePermissions.BLOCK,
              scope,
              notification.browser
            );
          }
        },
      },
    ];
  }

  // The formatMessagesSync method returns an array of results
  // for each message that was requested, and for the ones with
  // attributes, returns an attributes array with objects like:
  //     { name: "label", value: "somevalue" }
  const [mainMessage, ...secondaryMessages] = localization
    .formatMessagesSync(actionL10nIds)
    .map(msg =>
      msg.attributes.reduce(
        (acc, { name, value }) => ({ ...acc, [name]: value }),
        {}
      )
    );

  const mainAction = {
    label: mainMessage.label,
    accessKey: mainMessage.accesskey,
    // The real callback will be set during the "showing" event. The
    // empty function here is so that PopupNotifications.show doesn't
    // reject the action.
    callback() {},
  };

  for (let i = 0; i < secondaryActions.length; ++i) {
    secondaryActions[i].label = secondaryMessages[i].label;
    secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
  }

  let options = {
    name: originToShow,
    persistent: true,
    hideClose: true,
    eventCallback(aTopic, aNewBrowser, isCancel) {
      if (aTopic == "swapping") {
        return true;
      }

      let doc = this.browser.ownerDocument;

      // Clean-up video streams of screensharing previews.
      if (
        reqVideoInput !== "Screen" ||
        aTopic == "dismissed" ||
        aTopic == "removed"
      ) {
        let video = doc.getElementById("webRTC-previewVideo");
        video.deviceId = null; // Abort previews still being started.
        if (video.stream) {
          video.stream.getTracks().forEach(t => t.stop());
          video.stream = null;
          video.src = null;
          doc.getElementById("webRTC-preview").hidden = true;
        }
        let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
        if (menupopup._commandEventListener) {
          menupopup.removeEventListener(
            "command",
            menupopup._commandEventListener
          );
          menupopup._commandEventListener = null;
        }
      }

      if (aTopic == "removed" && notification && isCancel) {
        // The notification has been cancelled (e.g. due to entering
        // full-screen).  Also cancel the webRTC request.
        aActor.denyRequest(aRequest);
      } else if (
        aTopic == "shown" &&
        audioOutputDevices.length > 1 &&
        !notification.wasDismissed
      ) {
        // Focus the list on first show so that arrow keys select the speaker.
        doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
      }

      if (aTopic != "showing") {
        return false;
      }

      // If BLOCK has been set persistently in the permission manager or has
      // been set on the tab, then it is handled synchronously before we add
      // the notification.
      // Handling of ALLOW is delayed until the popupshowing event,
      // to avoid granting permissions automatically to background tabs.
      if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
        this.remove();
        return true;
      }

      /**
       * Prepare the device selector for one kind of device.
       * @param {Object[]} devices - available devices of this kind.
       * @param {string} IDPrefix - indicating kind of device and so
       *   associated UI elements.
       * @param {string[]} describedByIDs - an array to which might be
       *   appended ids of elements that describe the panel, for the caller to
       *   use in the aria-describedby attribute.
       */
      function listDevices(devices, IDPrefix, describedByIDs) {
        let labelID = `${IDPrefix}-single-device-label`;
        let list;
        let itemParent;
        if (IDPrefix == "webRTC-selectSpeaker") {
          list = doc.getElementById(`${IDPrefix}-richlistbox`);
          itemParent = list;
        } else {
          itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
          list = itemParent.parentNode; // menulist
        }
        while (itemParent.lastChild) {
          itemParent.removeChild(itemParent.lastChild);
        }

        // Removing the child nodes of a menupopup doesn't clear the value
        // attribute of its menulist. Similary for richlistbox state. This can
        // have unfortunate side effects when the list is rebuilt with a
        // different content, so we set the selectedIndex explicitly to reset
        // state.
        let defaultIndex = 0;

        for (let device of devices) {
          let item = addDeviceToList(list, device.name, device.deviceIndex);
          if (IDPrefix == "webRTC-selectSpeaker") {
            item.addEventListener("dblclick", event => {
              // Allow the chosen speakers via
              // .popup-notification-primary-button so that
              // "security.notification_enable_delay" is checked.
              event.target.closest("popupnotification").button.doCommand();
            });
            if (device.id == aRequest.audioOutputId) {
              defaultIndex = device.deviceIndex;
            }
          }
        }
        list.selectedIndex = defaultIndex;

        let label = doc.getElementById(labelID);
        if (devices.length == 1) {
          describedByIDs.push(`${IDPrefix}-icon`, labelID);
          label.value = devices[0].name;
          label.hidden = false;
          list.hidden = true;
        } else {
          label.hidden = true;
          list.hidden = false;
        }
      }

      let notificationElement = doc.getElementById(
        "webRTC-shareDevices-notification"
      );

      function checkDisabledWindowMenuItem() {
        let list = doc.getElementById("webRTC-selectWindow-menulist");
        let item = list.selectedItem;
        if (!item || item.hasAttribute("disabled")) {
          notificationElement.setAttribute("invalidselection", "true");
        } else {
          notificationElement.removeAttribute("invalidselection");
        }
      }

      function listScreenShareDevices(menupopup, devices) {
        while (menupopup.lastChild) {
          menupopup.removeChild(menupopup.lastChild);
        }

        // Removing the child nodes of the menupopup doesn't clear the value
        // attribute of the menulist. This can have unfortunate side effects
        // when the list is rebuilt with a different content, so we remove
        // the value attribute and unset the selectedItem explicitly.
        menupopup.parentNode.removeAttribute("value");
        menupopup.parentNode.selectedItem = null;

        // "Select a Window or Screen" is the default because we can't and don't
        // want to pick a 'default' window to share (Full screen is "scary").
        addDeviceToList(
          menupopup.parentNode,
          localization.formatValueSync("webrtc-pick-window-or-screen"),
          "-1"
        );
        menupopup.appendChild(doc.createXULElement("menuseparator"));

        let isPipeWireDetected = false;

        // Build the list of 'devices'.
        let monitorIndex = 1;
        for (let i = 0; i < devices.length; ++i) {
          let device = devices[i];
          let type = device.mediaSource;
          let name;
          if (device.canRequestOsLevelPrompt) {
            // When we share content by PipeWire add only one item to the device
            // list. When it's selected PipeWire portal dialog is opened and
            // user confirms actual window/screen sharing there.
            // Don't mark it as scary as there's an extra confirmation step by
            // PipeWire portal dialog.

            isPipeWireDetected = true;
            let item = addDeviceToList(
              menupopup.parentNode,
              localization.formatValueSync("webrtc-share-pipe-wire-portal"),
              i,
              type
            );
            item.deviceId = device.rawId;
            item.mediaSource = type;

            // In this case the OS sharing dialog will be the only option and
            // can be safely pre-selected.
            menupopup.parentNode.selectedItem = item;
            continue;
          } else if (type == "screen") {
            // Building screen list from available screens.
            if (device.name == "Primary Monitor") {
              name = localization.formatValueSync("webrtc-share-entire-screen");
            } else {
              name = localization.formatValueSync("webrtc-share-monitor", {
                monitorIndex,
              });
              ++monitorIndex;
            }
          } else {
            name = device.name;

            if (type == "application") {
              // The application names returned by the platform are of the form:
              // <window count>\x1e<application name>
              const [count, appName] = name.split("\x1e");
              name = localization.formatValueSync("webrtc-share-application", {
                appName,
                windowCount: parseInt(count),
              });
            }
          }
          let item = addDeviceToList(menupopup.parentNode, name, i, type);
          item.deviceId = device.rawId;
          item.mediaSource = type;
          if (device.scary) {
            item.scary = true;
          }
        }

        // Always re-select the "No <type>" item.
        doc
          .getElementById("webRTC-selectWindow-menulist")
          .removeAttribute("value");
        doc.getElementById("webRTC-all-windows-shared").hidden = true;

        menupopup._commandEventListener = event => {
          checkDisabledWindowMenuItem();
          let video = doc.getElementById("webRTC-previewVideo");
          if (video.stream) {
            video.stream.getTracks().forEach(t => t.stop());
            video.stream = null;
          }

          const { deviceId, mediaSource, scary } = event.target;
          if (deviceId == undefined) {
            doc.getElementById("webRTC-preview").hidden = true;
            video.src = null;
            return;
          }

          let warning = doc.getElementById("webRTC-previewWarning");
          let warningBox = doc.getElementById("webRTC-previewWarningBox");
          warningBox.hidden = !scary;
          let chromeWin = doc.defaultView;
          if (scary) {
            const warnId =
              mediaSource == "screen"
                ? "webrtc-share-screen-warning"
                : "webrtc-share-browser-warning";
            doc.l10n.setAttributes(warning, warnId);

            const learnMore = doc.getElementById(
              "webRTC-previewWarning-learnMore"
            );
            const baseURL = Services.urlFormatter.formatURLPref(
              "app.support.baseURL"
            );
            learnMore.setAttribute("href", baseURL + "screenshare-safety");
            doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");

            // On Catalina, we don't want to blow our chance to show the
            // OS-level helper prompt to enable screen recording if the user
            // intends to reject anyway. OTOH showing it when they click Allow
            // is too late. A happy middle is to show it when the user makes a
            // choice in the picker. This already happens implicitly if the
            // user chooses "Entire desktop", as a side-effect of our preview,
            // we just need to also do it if they choose "Firefox". These are
            // the lone two options when permission is absent on Catalina.
            // Ironically, these are the two sources marked "scary" from a
            // web-sharing perspective, which is why this code resides here.
            // A restart doesn't appear to be necessary in spite of OS wording.
            let scrStatus = {};
            lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
            if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
              lazy.OSPermissions.maybeRequestScreenCapturePermission();
            }
          }

          let perms = Services.perms;
          let chromePrincipal =
            Services.scriptSecurityManager.getSystemPrincipal();
          perms.addFromPrincipal(
            chromePrincipal,
            "MediaManagerVideo",
            perms.ALLOW_ACTION,
            perms.EXPIRE_SESSION
          );

          // We don't have access to any screen content besides our browser tabs
          // on Wayland, therefore there are no previews we can show.
          if (
            (!isPipeWireDetected || mediaSource == "browser") &&
            Services.prefs.getBoolPref(
              "media.getdisplaymedia.previews.enabled",
              true
            )
          ) {
            video.deviceId = deviceId;
            let constraints = {
              video: { mediaSource, deviceId: { exact: deviceId } },
            };
            chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
              stream => {
                if (video.deviceId != deviceId) {
                  // The user has selected a different device or closed the panel
                  // before getUserMedia finished.
                  stream.getTracks().forEach(t => t.stop());
                  return;
                }
                video.srcObject = stream;
                video.stream = stream;
                doc.getElementById("webRTC-preview").hidden = false;
                video.onloadedmetadata = function () {
                  video.play();
                };
              },
              err => {
                if (
                  err.name == "OverconstrainedError" &&
                  err.constraint == "deviceId"
                ) {
                  // Window has disappeared since enumeration, which can happen.
                  // No preview for you.
                  return;
                }
                console.error(
                  `error in preview: ${err.message} ${err.constraint}`
                );
              }
            );
          }
        };
        menupopup.addEventListener("command", menupopup._commandEventListener);
      }

      function addDeviceToList(list, deviceName, deviceIndex, type) {
        let item = list.appendItem(deviceName, deviceIndex);
        item.setAttribute("tooltiptext", deviceName);
        if (type) {
          item.setAttribute("devicetype", type);
        }

        if (deviceIndex == "-1") {
          item.setAttribute("disabled", true);
        }

        return item;
      }

      doc.getElementById("webRTC-selectCamera").hidden =
        reqVideoInput !== "Camera";
      doc.getElementById("webRTC-selectWindowOrScreen").hidden =
        reqVideoInput !== "Screen";
      doc.getElementById("webRTC-selectMicrophone").hidden =
        reqAudioInput !== "Microphone";
      doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;

      let describedByIDs = ["webRTC-shareDevices-notification-description"];

      if (sharingScreen) {
        let windowMenupopup = doc.getElementById(
          "webRTC-selectWindow-menupopup"
        );
        listScreenShareDevices(windowMenupopup, videoInputDevices);
        checkDisabledWindowMenuItem();
      } else {
        listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
        notificationElement.removeAttribute("invalidselection");
      }
      if (!sharingAudio) {
        listDevices(
          audioInputDevices,
          "webRTC-selectMicrophone",
          describedByIDs
        );
      }
      listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);

      // PopupNotifications knows to clear the aria-describedby attribute
      // when hiding, so we don't have to worry about cleaning it up ourselves.
      chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
        "aria-describedby",
        describedByIDs.join(" ")
      );

      this.mainAction.callback = async function (aState) {
        let remember = false;
        let silenceNotifications = false;

        if (notificationSilencingEnabled && sharingScreen) {
          silenceNotifications = aState && aState.checkboxChecked;
        } else {
          remember = aState && aState.checkboxChecked;
        }

        let allowedDevices = [];
        let perms = Services.perms;
        if (reqVideoInput) {
          let listId = sharingScreen
            ? "webRTC-selectWindow-menulist"
            : "webRTC-selectCamera-menulist";
          let videoDeviceIndex = doc.getElementById(listId).value;
          let allowVideoDevice = videoDeviceIndex != "-1";
          if (allowVideoDevice) {
            allowedDevices.push(videoDeviceIndex);
            // Session permission will be removed after use
            // (it's really one-shot, not for the entire session)
            perms.addFromPrincipal(
              principal,
              "MediaManagerVideo",
              perms.ALLOW_ACTION,
              perms.EXPIRE_SESSION
            );
            let { mediaSource, rawId } = videoInputDevices.find(
              ({ deviceIndex }) => deviceIndex == videoDeviceIndex
            );
            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
            if (!sharingScreen) {
              persistGrantOrPromptPermission(principal, "camera", remember);
            }
          }
        }

        if (reqAudioInput === "Microphone") {
          let audioDeviceIndex = doc.getElementById(
            "webRTC-selectMicrophone-menulist"
          ).value;
          let allowMic = audioDeviceIndex != "-1";
          if (allowMic) {
            allowedDevices.push(audioDeviceIndex);
            let { mediaSource, rawId } = audioInputDevices.find(
              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
            );
            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
            persistGrantOrPromptPermission(principal, "microphone", remember);
          }
        } else if (reqAudioInput === "AudioCapture") {
          // Only one device possible for audio capture.
          allowedDevices.push(0);
        }

        if (reqAudioOutput) {
          let audioDeviceIndex = doc.getElementById(
            "webRTC-selectSpeaker-richlistbox"
          ).value;
          let allowSpeaker = audioDeviceIndex != "-1";
          if (allowSpeaker) {
            allowedDevices.push(audioDeviceIndex);
            let { id } = audioOutputDevices.find(
              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
            );
            lazy.SitePermissions.setForPrincipal(
              principal,
              ["speaker", id].join("^"),
              lazy.SitePermissions.ALLOW
            );
          }
        }

        if (!allowedDevices.length) {
          aActor.denyRequest(aRequest);
          return;
        }

        const camNeeded = reqVideoInput === "Camera";
        const micNeeded = !!reqAudioInput;
        const scrNeeded = reqVideoInput === "Screen";
        const havePermission = await aActor.checkOSPermission(
          camNeeded,
          micNeeded,
          scrNeeded
        );
        if (!havePermission) {
          aActor.denyRequestNoPermission(aRequest);
          return;
        }

        aActor.sendAsyncMessage("webrtc:Allow", {
          callID: aRequest.callID,
          windowID: aRequest.windowID,
          devices: allowedDevices,
          suppressNotifications: silenceNotifications,
        });
      };

      // If we haven't handled the permission yet, we want to show the doorhanger.
      return false;
    },
  };

  function shouldShowAlwaysRemember() {
    // Don't offer "always remember" action in PB mode
    if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
      return false;
    }

    // Don't offer "always remember" action in maybe unsafe permission
    // delegation
    if (aRequest.secondOrigin) {
      return false;
    }

    // Speaker grants are always remembered, so no checkbox is required.
    if (reqAudioOutput) {
      return false;
    }

    return true;
  }

  function getRememberCheckboxLabel() {
    if (reqVideoInput == "Camera") {
      if (reqAudioInput == "Microphone") {
        return "webrtc-remember-allow-checkbox-camera-and-microphone";
      }
      return "webrtc-remember-allow-checkbox-camera";
    }

    if (reqAudioInput == "Microphone") {
      return "webrtc-remember-allow-checkbox-microphone";
    }

    return "webrtc-remember-allow-checkbox";
  }

  if (shouldShowAlwaysRemember()) {
    // Disable the permanent 'Allow' action if the connection isn't secure, or for
    // screen/audio sharing (because we can't guess which window the user wants to
    // share without prompting). Note that we never enter this block for private
    // browsing windows.
    let reason = "";
    if (sharingScreen) {
      reason = "webrtc-reason-for-no-permanent-allow-screen";
    } else if (sharingAudio) {
      reason = "webrtc-reason-for-no-permanent-allow-audio";
    } else if (!aRequest.secure) {
      reason = "webrtc-reason-for-no-permanent-allow-insecure";
    }

    options.checkbox = {
      label: localization.formatValueSync(getRememberCheckboxLabel()),
      checked: principal.isAddonOrExpandedAddonPrincipal,
      checkedState: reason
        ? {
            disableMainAction: true,
            warningLabel: localization.formatValueSync(reason),
          }
        : undefined,
    };
  }

  // If the notification silencing feature is enabled and we're sharing a
  // screen, then the checkbox for the permission panel is what controls
  // notification silencing.
  if (notificationSilencingEnabled && sharingScreen) {
    options.checkbox = {
      label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
      checked: false,
      checkedState: {
        disableMainAction: false,
      },
    };
  }

  let anchorId = "webRTC-shareDevices-notification-icon";
  if (reqVideoInput === "Screen") {
    anchorId = "webRTC-shareScreen-notification-icon";
  } else if (!reqVideoInput) {
    if (reqAudioInput && !reqAudioOutput) {
      anchorId = "webRTC-shareMicrophone-notification-icon";
    } else if (!reqAudioInput && reqAudioOutput) {
      anchorId = "webRTC-shareSpeaker-notification-icon";
    }
  }

  if (aRequest.secondOrigin) {
    options.secondName = lazy.webrtcUI.getHostOrExtensionName(
      null,
      aRequest.secondOrigin
    );
  }

  notification = chromeDoc.defaultView.PopupNotifications.show(
    aBrowser,
    "webRTC-shareDevices",
    message,
    anchorId,
    mainAction,
    secondaryActions,
    options
  );
  notification.callID = aRequest.callID;
}

/**
 * @param {"Screen" | "Camera" | null} reqVideoInput
 * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
 * @param {boolean} reqAudioOutput
 * @param {boolean} delegation - Is the access delegated to a third party?
 * @returns {string} Localization message identifier
 */
function getPromptMessageId(
  reqVideoInput,
  reqAudioInput,
  reqAudioOutput,
  delegation
) {
  switch (reqVideoInput) {
    case "Camera":
      switch (reqAudioInput) {
        case "Microphone":
          return delegation
            ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
            : "webrtc-allow-share-camera-and-microphone";
        case "AudioCapture":
          return delegation
            ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
            : "webrtc-allow-share-camera-and-audio-capture";
        default:
          return delegation
            ? "webrtc-allow-share-camera-unsafe-delegation"
            : "webrtc-allow-share-camera";
      }

    case "Screen":
      switch (reqAudioInput) {
        case "Microphone":
          return delegation
            ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
            : "webrtc-allow-share-screen-and-microphone";
        case "AudioCapture":
          return delegation
            ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
            : "webrtc-allow-share-screen-and-audio-capture";
        default:
          return delegation
            ? "webrtc-allow-share-screen-unsafe-delegation"
            : "webrtc-allow-share-screen";
      }

    default:
      switch (reqAudioInput) {
        case "Microphone":
          return delegation
            ? "webrtc-allow-share-microphone-unsafe-delegation"
            : "webrtc-allow-share-microphone";
        case "AudioCapture":
          return delegation
            ? "webrtc-allow-share-audio-capture-unsafe-delegation"
            : "webrtc-allow-share-audio-capture";
        default:
          // This should be always true, if we've reached this far.
          if (reqAudioOutput) {
            return delegation
              ? "webrtc-allow-share-speaker-unsafe-delegation"
              : "webrtc-allow-share-speaker";
          }
          return undefined;
      }
  }
}

/**
 * Checks whether we have a microphone/camera in use by checking the activePerms map
 * or if we have an allow permission for a microphone/camera in sitePermissions
 * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
 * @return true if one of the above conditions is met
 */
function allowedOrActiveCameraOrMicrophone(browser) {
  // Do we have an allow permission for cam/mic in the permissions manager?
  if (
    lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
      return (
        perm.state == lazy.SitePermissions.ALLOW &&
        (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
      );
    })
  ) {
    // Return early, no need to check for active devices
    return true;
  }

  // Do we have an active device?
  return (
    // Find all windowIDs that belong to our browsing contexts
    browser.browsingContext
      .getAllBrowsingContextsInSubtree()
      // Only keep the outerWindowIds
      .map(bc => bc.currentWindowGlobal?.outerWindowId)
      .filter(id => id != null)
      // We have an active device if one of our windowIds has a non empty map in the activePerms map
      // that includes one device of type "camera" or "microphone"
      .some(id => {
        let map = lazy.webrtcUI.activePerms.get(id);
        if (!map) {
          // This windowId has no active device
          return false;
        }
        // Let's see if one of the devices is a camera or a microphone
        let types = [...map.values()];
        return types.includes("microphone") || types.includes("camera");
      })
  );
}

function removePrompt(aBrowser, aCallId) {
  let chromeWin = aBrowser.ownerGlobal;
  let notification = chromeWin.PopupNotifications.getNotification(
    "webRTC-shareDevices",
    aBrowser
  );
  if (notification && notification.callID == aCallId) {
    notification.remove();
  }
}

/**
 * Clears temporary permission grants used for WebRTC device grace periods.
 * @param browser - Browser element to clear permissions for.
 * @param {boolean} clearCamera - Clear camera grants.
 * @param {boolean} clearMicrophone - Clear microphone grants.
 */
function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
  if (!clearCamera && !clearMicrophone) {
    // Nothing to clear.
    return;
  }
  let perms = lazy.SitePermissions.getAllForBrowser(browser);
  perms
    .filter(perm => {
      let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
      // We only want to clear WebRTC grace periods. These are temporary, device
      // specifc (double-keyed) microphone or camera permissions.
      return (
        key &&
        perm.state == lazy.SitePermissions.ALLOW &&
        perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
        ((clearCamera && id == "camera") ||
          (clearMicrophone && id == "microphone"))
      );
    })
    .forEach(perm =>
      lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
    );
}

/**
 * Persist an ALLOW state if the remember option is true.
 * Otherwise, persist PROMPT so that we can later tell the site
 * that permission was granted once before.
 * This makes Firefox seem much more like Chrome to sites that
 * expect a one-off, persistent permission grant for cam/mic.
 *
 * @param principal - Principal to add permission to.
 * @param {string} permissionName - name of permission.
 * @param remember - whether the grant should be persisted.
 */
function persistGrantOrPromptPermission(principal, permissionName, remember) {
  // There are cases like unsafe delegation where a prompt appears
  // even in ALLOW state, so make sure to not overwrite it (there's
  // no remember checkbox in those cases)
  if (
    lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
    lazy.SitePermissions.ALLOW
  ) {
    return;
  }

  lazy.SitePermissions.setForPrincipal(
    principal,
    permissionName,
    remember ? lazy.SitePermissions.ALLOW : lazy.SitePermissions.PROMPT
  );
}

/**
 * Clears any persisted PROMPT (aka Always Ask) permission.
 * @param principal - Principal to remove permission from.
 * @param {string} permissionName - name of permission.
 * @param browser - Browser element to clear permission for.
 */
function maybeClearAlwaysAsk(principal, permissionName, browser) {
  // For the "Always Ask" user choice, only persisted PROMPT is used,
  // so no need to scan through temporary permissions.
  if (
    lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
    lazy.SitePermissions.PROMPT
  ) {
    lazy.SitePermissions.removeFromPrincipal(
      principal,
      permissionName,
      browser
    );
  }
}

[ zur Elbe Produktseite wechseln0.69Quellennavigators  Analyse erneut starten  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge