tor-browser

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

webrtcIndicator.js (18763B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const { XPCOMUtils } = ChromeUtils.importESModule(
      6  "resource://gre/modules/XPCOMUtils.sys.mjs"
      7 );
      8 const { AppConstants } = ChromeUtils.importESModule(
      9  "resource://gre/modules/AppConstants.sys.mjs"
     10 );
     11 const { showStreamSharingMenu, webrtcUI } = ChromeUtils.importESModule(
     12  "resource:///modules/webrtcUI.sys.mjs"
     13 );
     14 
     15 XPCOMUtils.defineLazyServiceGetter(
     16  this,
     17  "gScreenManager",
     18  "@mozilla.org/gfx/screenmanager;1",
     19  Ci.nsIScreenManager
     20 );
     21 
     22 /**
     23 * Public function called by webrtcUI to update the indicator
     24 * display when the active streams change.
     25 */
     26 function updateIndicatorState() {
     27  WebRTCIndicator.updateIndicatorState();
     28 }
     29 
     30 /**
     31 * Public function called by webrtcUI to indicate that webrtcUI
     32 * is about to close the indicator. This is so that we can differentiate
     33 * between closes that are caused by webrtcUI, and closes that are
     34 * caused by other reasons (like the user closing the window via the
     35 * OS window controls somehow).
     36 *
     37 * If the window is closed without having called this method first, the
     38 * indicator will ask webrtcUI to shutdown any remaining streams and then
     39 * select and focus the most recent browser tab that a stream was shared
     40 * with.
     41 */
     42 function closingInternally() {
     43  WebRTCIndicator.closingInternally();
     44 }
     45 
     46 /**
     47 * Main control object for the WebRTC global indicator
     48 */
     49 const WebRTCIndicator = {
     50  init() {
     51    addEventListener("load", this);
     52    addEventListener("unload", this);
     53 
     54    // If the user customizes the position of the indicator, we will
     55    // not try to re-center it on the primary display after indicator
     56    // state updates.
     57    this.positionCustomized = false;
     58 
     59    this.updatingIndicatorState = false;
     60    this.loaded = false;
     61    this.isClosingInternally = false;
     62 
     63    this.statusBar = null;
     64    this.statusBarMenus = new Set();
     65 
     66    this.showGlobalMuteToggles = Services.prefs.getBoolPref(
     67      "privacy.webrtc.globalMuteToggles",
     68      false
     69    );
     70 
     71    this.hideGlobalIndicator =
     72      Services.prefs.getBoolPref("privacy.webrtc.hideGlobalIndicator", false) ||
     73      Services.appinfo.isWayland;
     74 
     75    if (this.hideGlobalIndicator) {
     76      this.setVisibility(false);
     77    }
     78  },
     79 
     80  /**
     81   * Controls the visibility of the global indicator. Also sets the value of
     82   * a "visible" attribute on the document element to "true" or "false".
     83   *
     84   * @param isVisible (boolean)
     85   *   Whether or not the global indicator should be visible.
     86   */
     87  setVisibility(isVisible) {
     88    let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
     89    baseWin.visibility = isVisible;
     90    // AppWindow::GetVisibility _always_ returns true (see
     91    // https://bugzilla.mozilla.org/show_bug.cgi?id=306245), so we'll set an
     92    // attribute on the document to make it easier for tests to know that the
     93    // indicator is not visible.
     94    document.documentElement.setAttribute("visible", isVisible);
     95  },
     96 
     97  /**
     98   * Exposed externally so that webrtcUI can alert the indicator to
     99   * update itself when sharing states have changed.
    100   */
    101  updateIndicatorState() {
    102    // It's possible that we were called externally before the indicator
    103    // finished loading. If so, then bail out - we're going to call
    104    // updateIndicatorState ourselves automatically once the load
    105    // event fires.
    106    if (!this.loaded) {
    107      return;
    108    }
    109 
    110    // We've started to update the indicator state. We set this flag so
    111    // that the MozUpdateWindowPos event handler doesn't interpret indicator
    112    // state updates as window movement caused by the user.
    113    this.updatingIndicatorState = true;
    114 
    115    let showCameraIndicator = webrtcUI.showCameraIndicator;
    116    let showMicrophoneIndicator = webrtcUI.showMicrophoneIndicator;
    117    let showScreenSharingIndicator = webrtcUI.showScreenSharingIndicator;
    118    if (this.statusBar) {
    119      let statusMenus = new Map([
    120        ["Camera", showCameraIndicator],
    121        ["Microphone", showMicrophoneIndicator],
    122        ["Screen", showScreenSharingIndicator],
    123      ]);
    124 
    125      for (let [name, shouldShow] of statusMenus) {
    126        let menu = document.getElementById(`webRTC-sharing${name}-menu`);
    127        if (shouldShow && !this.statusBarMenus.has(menu)) {
    128          this.statusBar.addItem(menu);
    129          this.statusBarMenus.add(menu);
    130        } else if (!shouldShow && this.statusBarMenus.has(menu)) {
    131          this.statusBar.removeItem(menu);
    132          this.statusBarMenus.delete(menu);
    133        }
    134      }
    135    }
    136 
    137    if (!this.showGlobalMuteToggles && !webrtcUI.showScreenSharingIndicator) {
    138      this.setVisibility(false);
    139    } else if (!this.hideGlobalIndicator) {
    140      this.setVisibility(true);
    141    }
    142 
    143    if (this.showGlobalMuteToggles) {
    144      this.updateWindowAttr("sharingvideo", showCameraIndicator);
    145      this.updateWindowAttr("sharingaudio", showMicrophoneIndicator);
    146    }
    147 
    148    let sharingScreen = showScreenSharingIndicator.startsWith("Screen");
    149    this.updateWindowAttr("sharingscreen", sharingScreen);
    150 
    151    let sharingTab = showScreenSharingIndicator.startsWith("Browser");
    152 
    153    // We special-case sharing a window, because we want to have a slightly
    154    // different UI if we're sharing a browser window.
    155    let sharingWindow = showScreenSharingIndicator.startsWith("Window");
    156    this.updateWindowAttr("sharingwindow", sharingWindow);
    157 
    158    let sharingBrowserWindow = false;
    159 
    160    if (sharingWindow) {
    161      // Get the active window streams and see if any of them are "scary".
    162      // If so, then we're sharing a browser window.
    163      let activeStreams = webrtcUI.getActiveStreams(
    164        false /* camera */,
    165        false /* microphone */,
    166        false /* screen */,
    167        false /* tab */,
    168        true /* window */
    169      );
    170      sharingBrowserWindow = activeStreams.some(({ devices }) =>
    171        devices.some(({ scary }) => scary)
    172      );
    173    }
    174    this.updateWindowAttr("sharingtab", sharingTab || sharingBrowserWindow);
    175 
    176    // The label that's displayed when sharing a display followed a priority.
    177    // The more "risky" we deem the display is for sharing, the higher priority.
    178    // This gives us the following priorities, from highest to lowest.
    179    //
    180    // 1. Screen
    181    // 2. Browser window or tab
    182    // 3. Other application window
    183    //
    184    // The CSS for the indicator does the work of showing or hiding these labels
    185    // for us, but we need to update the aria-labelledby attribute on the container
    186    // of those labels to make it clearer for screenreaders which one the user cares
    187    // about.
    188    let displayShare = document.getElementById("display-share");
    189    let labelledBy;
    190    if (sharingScreen) {
    191      labelledBy = "screen-share-info";
    192    } else if (sharingBrowserWindow || sharingTab) {
    193      // TODO: Need to decide if we need different label for tab
    194      labelledBy = "browser-window-share-info";
    195    } else if (sharingWindow) {
    196      labelledBy = "window-share-info";
    197    }
    198    displayShare.setAttribute("aria-labelledby", labelledBy);
    199 
    200    if (window.windowState != window.STATE_MINIMIZED) {
    201      // Resize and ensure the window position is correct
    202      // (sizeToContent messes with our position).
    203      let docElStyle = document.documentElement.style;
    204      docElStyle.minWidth = docElStyle.maxWidth = "unset";
    205      docElStyle.minHeight = docElStyle.maxHeight = "unset";
    206      window.sizeToContent();
    207 
    208      // On Linux GTK, the style of window we're using by default is resizable. We
    209      // workaround this by setting explicit limits on the height and width of the
    210      // window.
    211      if (AppConstants.platform == "linux") {
    212        let { width, height } = window.windowUtils.getBoundsWithoutFlushing(
    213          document.documentElement
    214        );
    215 
    216        docElStyle.minWidth = docElStyle.maxWidth = `${width}px`;
    217        docElStyle.minHeight = docElStyle.maxHeight = `${height}px`;
    218      }
    219 
    220      this.ensureOnScreen();
    221 
    222      if (!this.positionCustomized) {
    223        this.centerOnLatestBrowser();
    224      }
    225    }
    226 
    227    this.updatingIndicatorState = false;
    228  },
    229 
    230  /**
    231   * After the indicator has been updated, checks to see if it has expanded
    232   * such that part of the indicator is now outside of the screen. If so,
    233   * it then adjusts the position to put the entire indicator on screen.
    234   */
    235  ensureOnScreen() {
    236    let desiredX = Math.max(window.screenX, screen.availLeft);
    237    let maxX =
    238      screen.availLeft +
    239      screen.availWidth -
    240      document.documentElement.clientWidth;
    241    window.moveTo(Math.min(desiredX, maxX), window.screenY);
    242  },
    243 
    244  /**
    245   * If the indicator is first being opened, we'll find the browser window
    246   * associated with the most recent share, and pin the indicator to the
    247   * very top of the content area.
    248   */
    249  centerOnLatestBrowser() {
    250    let activeStreams = webrtcUI.getActiveStreams(
    251      true /* camera */,
    252      true /* microphone */,
    253      true /* screen */,
    254      true /* tab */,
    255      true /* window */
    256    );
    257 
    258    if (!activeStreams.length) {
    259      return;
    260    }
    261 
    262    let browser = activeStreams[activeStreams.length - 1].browser;
    263    let browserWindow = browser.ownerGlobal;
    264    let browserRect =
    265      browserWindow.windowUtils.getBoundsWithoutFlushing(browser);
    266 
    267    // This should be called in initialize right after we've just called
    268    // updateIndicatorState. Since updateIndicatorState uses
    269    // window.sizeToContent, the layout information should be up to date,
    270    // and so the numbers that we get without flushing should be sufficient.
    271    let { width: windowWidth } = window.windowUtils.getBoundsWithoutFlushing(
    272      document.documentElement
    273    );
    274 
    275    window.moveTo(
    276      browserWindow.mozInnerScreenX +
    277        browserRect.left +
    278        (browserRect.width - windowWidth) / 2,
    279      browserWindow.mozInnerScreenY + browserRect.top
    280    );
    281  },
    282 
    283  handleEvent(event) {
    284    switch (event.type) {
    285      case "load": {
    286        this.onLoad();
    287        break;
    288      }
    289      case "unload": {
    290        this.onUnload();
    291        break;
    292      }
    293      case "click": {
    294        this.onClick(event);
    295        break;
    296      }
    297      case "change": {
    298        this.onChange(event);
    299        break;
    300      }
    301      case "MozUpdateWindowPos": {
    302        if (!this.updatingIndicatorState) {
    303          // The window moved while not updating the indicator state,
    304          // so the user probably moved it.
    305          this.positionCustomized = true;
    306        }
    307        break;
    308      }
    309      case "sizemodechange": {
    310        if (window.windowState != window.STATE_MINIMIZED) {
    311          this.updateIndicatorState();
    312        }
    313        break;
    314      }
    315      case "popupshowing": {
    316        this.onPopupShowing(event);
    317        break;
    318      }
    319      case "popuphiding": {
    320        this.onPopupHiding(event);
    321        break;
    322      }
    323      case "command": {
    324        this.onCommand(event);
    325        break;
    326      }
    327      case "DOMWindowClose":
    328      case "close": {
    329        this.onClose(event);
    330        break;
    331      }
    332    }
    333  },
    334 
    335  onLoad() {
    336    this.loaded = true;
    337 
    338    if (AppConstants.platform == "macosx" || AppConstants.platform == "win") {
    339      this.statusBar = Cc["@mozilla.org/widget/systemstatusbar;1"].getService(
    340        Ci.nsISystemStatusBar
    341      );
    342    }
    343 
    344    this.updateIndicatorState();
    345 
    346    window.addEventListener("click", this);
    347    window.addEventListener("change", this);
    348    window.addEventListener("sizemodechange", this);
    349 
    350    // There are two ways that the dialog can close - either via the
    351    // .close() window method, or via the OS. We handle both of those
    352    // cases here.
    353    window.addEventListener("DOMWindowClose", this);
    354    window.addEventListener("close", this);
    355 
    356    if (this.statusBar) {
    357      // We only want these events for the system status bar menus.
    358      window.addEventListener("popupshowing", this);
    359      window.addEventListener("popuphiding", this);
    360      window.addEventListener("command", this);
    361    }
    362 
    363    window.windowRoot.addEventListener("MozUpdateWindowPos", this);
    364 
    365    // Alert accessibility implementations stuff just changed. We only need to do
    366    // this initially, because changes after this will automatically fire alert
    367    // events if things change materially.
    368    let ev = new CustomEvent("AlertActive", {
    369      bubbles: true,
    370      cancelable: true,
    371    });
    372    document.documentElement.dispatchEvent(ev);
    373 
    374    this.loaded = true;
    375  },
    376 
    377  onClose(event) {
    378    // This event is fired from when the indicator window tries to be closed.
    379    // If we preventDefault() the event, we are able to cancel that close
    380    // attempt.
    381    //
    382    // We want to do that if we're not showing the global mute toggles
    383    // and we're still sharing a camera or a microphone so that we can
    384    // keep the status bar indicators present (since those status bar
    385    // indicators are bound to this window).
    386    if (
    387      !this.showGlobalMuteToggles &&
    388      (webrtcUI.showCameraIndicator || webrtcUI.showMicrophoneIndicator)
    389    ) {
    390      event.preventDefault();
    391      this.setVisibility(false);
    392    }
    393 
    394    if (!this.isClosingInternally) {
    395      // Something has tried to close the indicator, but it wasn't webrtcUI.
    396      // This means we might still have some streams being shared. To protect
    397      // the user from unknowingly sharing streams, we shut those streams
    398      // down.
    399      //
    400      // This only includes the camera and microphone streams if the user
    401      // has the global mute toggles enabled, since these toggles visually
    402      // associate the indicator with those streams.
    403      let activeStreams = webrtcUI.getActiveStreams(
    404        this.showGlobalMuteToggles /* camera */,
    405        this.showGlobalMuteToggles /* microphone */,
    406        true /* screen */,
    407        true /* tab */,
    408        true /* window */
    409      );
    410      webrtcUI.stopSharingStreams(
    411        activeStreams,
    412        this.showGlobalMuteToggles /* camera */,
    413        this.showGlobalMuteToggles /* microphone */,
    414        true /* screen */,
    415        true /* window */
    416      );
    417    }
    418  },
    419 
    420  onUnload() {
    421    Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", false);
    422    Services.ppmm.sharedData.set("WebRTC:GlobalMicrophoneMute", false);
    423    Services.ppmm.sharedData.flush();
    424 
    425    if (this.statusBar) {
    426      for (let menu of this.statusBarMenus) {
    427        this.statusBar.removeItem(menu);
    428      }
    429    }
    430  },
    431 
    432  onClick(event) {
    433    switch (event.target.id) {
    434      case "stop-sharing": {
    435        let activeStreams = webrtcUI.getActiveStreams(
    436          false /* camera */,
    437          false /* microphone */,
    438          true /* screen */,
    439          true /* tab */,
    440          true /* window */
    441        );
    442 
    443        if (!activeStreams.length) {
    444          return;
    445        }
    446 
    447        // getActiveStreams is filtering for streams that have screen
    448        // sharing, but those streams might _also_ be sharing other
    449        // devices like camera or microphone. This is why we need to
    450        // tell stopSharingStreams explicitly which device type we want
    451        // to stop.
    452        webrtcUI.stopSharingStreams(
    453          activeStreams,
    454          false /* camera */,
    455          false /* microphone */,
    456          true /* screen */,
    457          true /* window */
    458        );
    459        break;
    460      }
    461      case "minimize": {
    462        window.minimize();
    463        break;
    464      }
    465    }
    466  },
    467 
    468  onChange(event) {
    469    switch (event.target.id) {
    470      case "microphone-mute-toggle": {
    471        this.toggleMicrophoneMute(event.target);
    472        break;
    473      }
    474      case "camera-mute-toggle": {
    475        this.toggleCameraMute(event.target);
    476        break;
    477      }
    478    }
    479  },
    480 
    481  onPopupShowing(event) {
    482    if (this.eventIsForDeviceMenuPopup(event)) {
    483      // When the indicator is hidden by default, opening the menu from the
    484      // system tray _might_ cause the indicator to try to become visible again.
    485      // We work around this by re-hiding it if it wasn't already visible.
    486      if (document.documentElement.getAttribute("visible") != "true") {
    487        let baseWin = window.docShell.treeOwner.QueryInterface(
    488          Ci.nsIBaseWindow
    489        );
    490        baseWin.visibility = false;
    491      }
    492 
    493      showStreamSharingMenu(window, event, true);
    494    }
    495  },
    496 
    497  onPopupHiding(event) {
    498    if (!this.eventIsForDeviceMenuPopup(event)) {
    499      return;
    500    }
    501 
    502    let menu = event.target;
    503    while (menu.firstChild) {
    504      menu.firstChild.remove();
    505    }
    506  },
    507 
    508  onCommand(event) {
    509    webrtcUI.showSharingDoorhanger(event.target.stream, event);
    510  },
    511 
    512  /**
    513   * Returns true if an event was fired for one of the shared device
    514   * menupopups.
    515   *
    516   * @param event (Event)
    517   *   The event to check.
    518   * @returns True if the event was for one of the shared device
    519   *   menupopups.
    520   */
    521  eventIsForDeviceMenuPopup(event) {
    522    let menupopup = event.target;
    523    let type = menupopup.getAttribute("type");
    524 
    525    return ["Camera", "Microphone", "Screen"].includes(type);
    526  },
    527 
    528  /**
    529   * Mutes or unmutes the microphone globally based on the checked
    530   * state of toggleEl. Also updates the tooltip of toggleEl once
    531   * the state change is done.
    532   *
    533   * @param toggleEl (Element)
    534   *   The input[type="checkbox"] for toggling the microphone mute
    535   *   state.
    536   */
    537  toggleMicrophoneMute(toggleEl) {
    538    Services.ppmm.sharedData.set(
    539      "WebRTC:GlobalMicrophoneMute",
    540      toggleEl.checked
    541    );
    542    Services.ppmm.sharedData.flush();
    543    let l10nId = toggleEl.checked
    544      ? "webrtc-microphone-muted"
    545      : "webrtc-microphone-unmuted";
    546    document.l10n.setAttributes(toggleEl, l10nId);
    547  },
    548 
    549  /**
    550   * Mutes or unmutes the camera globally based on the checked
    551   * state of toggleEl. Also updates the tooltip of toggleEl once
    552   * the state change is done.
    553   *
    554   * @param toggleEl (Element)
    555   *   The input[type="checkbox"] for toggling the camera mute
    556   *   state.
    557   */
    558  toggleCameraMute(toggleEl) {
    559    Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", toggleEl.checked);
    560    Services.ppmm.sharedData.flush();
    561    let l10nId = toggleEl.checked
    562      ? "webrtc-camera-muted"
    563      : "webrtc-camera-unmuted";
    564    document.l10n.setAttributes(toggleEl, l10nId);
    565  },
    566 
    567  /**
    568   * Updates an attribute on the <window> element.
    569   *
    570   * @param attr (String)
    571   *   The name of the attribute to update.
    572   * @param value (String?)
    573   *   A string to set the attribute to. If the value is false-y,
    574   *   the attribute is removed.
    575   */
    576  updateWindowAttr(attr, value) {
    577    let docEl = document.documentElement;
    578    if (value) {
    579      docEl.setAttribute(attr, "true");
    580    } else {
    581      docEl.removeAttribute(attr);
    582    }
    583  },
    584 
    585  /**
    586   * See the documentation on the script global closingInternally() function.
    587   */
    588  closingInternally() {
    589    this.isClosingInternally = true;
    590  },
    591 };
    592 
    593 WebRTCIndicator.init();