tor-browser

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

browser-fullScreenAndPointerLock.js (31210B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 var PointerlockFsWarning = {
      7  _element: null,
      8  _origin: null,
      9 
     10  /**
     11   * Timeout object for managing timeout request. If it is started when
     12   * the previous call hasn't finished, it would automatically cancelled
     13   * the previous one.
     14   */
     15  Timeout: class {
     16    constructor(func, delay) {
     17      this._id = 0;
     18      this._func = func;
     19      this._delay = delay;
     20    }
     21    start() {
     22      this.cancel();
     23      this._id = setTimeout(() => this._handle(), this._delay);
     24    }
     25    cancel() {
     26      if (this._id) {
     27        clearTimeout(this._id);
     28        this._id = 0;
     29      }
     30    }
     31    _handle() {
     32      this._id = 0;
     33      this._func();
     34    }
     35    get delay() {
     36      return this._delay;
     37    }
     38  },
     39 
     40  showPointerLock(aOrigin) {
     41    if (!document.fullscreen) {
     42      let timeout = Services.prefs.getIntPref(
     43        "pointer-lock-api.warning.timeout"
     44      );
     45      this.show(aOrigin, "pointerlock-warning", timeout, 0);
     46    }
     47  },
     48 
     49  showFullScreen(aOrigin) {
     50    let timeout = Services.prefs.getIntPref("full-screen-api.warning.timeout");
     51    let delay = Services.prefs.getIntPref("full-screen-api.warning.delay");
     52    this.show(aOrigin, "fullscreen-warning", timeout, delay);
     53  },
     54 
     55  // Shows a warning that the site has entered fullscreen or
     56  // pointer lock for a short duration.
     57  show(aOrigin, elementId, timeout, delay) {
     58    if (!this._element) {
     59      this._element = document.getElementById(elementId);
     60      // Setup event listeners
     61      this._element.addEventListener("transitionend", this);
     62      this._element.addEventListener("transitioncancel", this);
     63      window.addEventListener("mousemove", this, true);
     64      // If the user explicitly disables the prompt, there's no need to detect
     65      // activation.
     66      if (timeout > 0) {
     67        window.addEventListener("activate", this);
     68        window.addEventListener("deactivate", this);
     69      }
     70      // The timeout to hide the warning box after a while.
     71      this._timeoutHide = new this.Timeout(() => {
     72        window.removeEventListener("activate", this);
     73        window.removeEventListener("deactivate", this);
     74        this._state = "hidden";
     75      }, timeout);
     76      // The timeout to show the warning box when the pointer is at the top
     77      this._timeoutShow = new this.Timeout(() => {
     78        this._state = "ontop";
     79        this._timeoutHide.start();
     80      }, delay);
     81    }
     82 
     83    // Set the strings on the warning UI.
     84    if (aOrigin) {
     85      this._origin = aOrigin;
     86    }
     87    let uri = Services.io.newURI(this._origin);
     88    let host = null;
     89    // Make an exception for PDF.js - we'll show "This document" instead.
     90    if (this._origin != "resource://pdf.js") {
     91      try {
     92        host = uri.host;
     93      } catch (e) {}
     94    }
     95    let textElem = this._element.querySelector(
     96      ".pointerlockfswarning-domain-text"
     97    );
     98    if (!host) {
     99      textElem.hidden = true;
    100    } else {
    101      textElem.removeAttribute("hidden");
    102      // Document's principal's URI has a host. Display a warning including it.
    103      let displayHost = BrowserUtils.formatURIForDisplay(uri, {
    104        onlyBaseDomain: true,
    105      });
    106      let l10nString = {
    107        "fullscreen-warning": "fullscreen-warning-domain",
    108        "pointerlock-warning": "pointerlock-warning-domain",
    109      }[elementId];
    110      document.l10n.setAttributes(textElem, l10nString, {
    111        domain: displayHost,
    112      });
    113    }
    114 
    115    this._element.dataset.identity =
    116      gIdentityHandler.pointerlockFsWarningClassName;
    117 
    118    // User should be allowed to explicitly disable
    119    // the prompt if they really want.
    120    if (this._timeoutHide.delay <= 0) {
    121      return;
    122    }
    123 
    124    if (Services.focus.activeWindow == window) {
    125      this._state = "onscreen";
    126      this._timeoutHide.start();
    127    }
    128  },
    129 
    130  /**
    131   * Close the full screen or pointerlock warning.
    132   *
    133   * @param {('fullscreen-warning'|'pointerlock-warning')} elementId - Id of the
    134   * warning element to close. If the id does not match the currently shown
    135   * warning this is a no-op.
    136   */
    137  close(elementId) {
    138    if (!elementId) {
    139      throw new Error("Must pass id of warning element to close");
    140    }
    141    if (!this._element || this._element.id != elementId) {
    142      return;
    143    }
    144    // Cancel any pending timeout
    145    this._timeoutHide.cancel();
    146    this._timeoutShow.cancel();
    147    // Reset state of the warning box
    148    this._state = "hidden";
    149    this._doHide();
    150    // Reset state of the text so we don't persist or retranslate it.
    151    this._element
    152      .querySelector(".pointerlockfswarning-domain-text")
    153      .removeAttribute("data-l10n-id");
    154    // Remove all event listeners
    155    this._element.removeEventListener("transitionend", this);
    156    this._element.removeEventListener("transitioncancel", this);
    157    window.removeEventListener("mousemove", this, true);
    158    window.removeEventListener("activate", this);
    159    window.removeEventListener("deactivate", this);
    160    // Clear fields
    161    this._element = null;
    162    this._timeoutHide = null;
    163    this._timeoutShow = null;
    164 
    165    // Ensure focus switches away from the (now hidden) warning box.
    166    // If the user clicked buttons in the warning box, it would have
    167    // been focused, and any key events would be directed at the (now
    168    // hidden) chrome document instead of the target document.
    169    gBrowser.selectedBrowser.focus();
    170  },
    171 
    172  // State could be one of "onscreen", "ontop", "hiding", and
    173  // "hidden". Setting the state to "onscreen" and "ontop" takes
    174  // effect immediately, while setting it to "hidden" actually
    175  // turns the state to "hiding" before the transition finishes.
    176  _lastState: null,
    177  _STATES: ["hidden", "ontop", "onscreen"],
    178  get _state() {
    179    for (let state of this._STATES) {
    180      if (this._element.hasAttribute(state)) {
    181        return state;
    182      }
    183    }
    184    return "hiding";
    185  },
    186 
    187  _doHide() {
    188    try {
    189      this._element.hidePopover();
    190    } catch (e) {}
    191    this._element.hidden = true;
    192  },
    193 
    194  set _state(newState) {
    195    let currentState = this._state;
    196    if (currentState == newState) {
    197      return;
    198    }
    199    if (currentState != "hiding") {
    200      this._lastState = currentState;
    201      this._element.removeAttribute(currentState);
    202    }
    203    if (currentState == "hidden") {
    204      this._element.showPopover();
    205    }
    206    // hidden is dealt with on transitionend or close(), see _doHide().
    207    if (newState != "hidden") {
    208      this._element.setAttribute(newState, "");
    209    }
    210  },
    211 
    212  handleEvent(event) {
    213    switch (event.type) {
    214      case "mousemove": {
    215        let state = this._state;
    216        if (state == "hidden") {
    217          // If the warning box is currently hidden, show it after
    218          // a short delay if the pointer is at the top.
    219          if (event.clientY != 0) {
    220            this._timeoutShow.cancel();
    221          } else if (this._timeoutShow.delay >= 0) {
    222            this._timeoutShow.start();
    223          }
    224        } else if (state != "onscreen") {
    225          let elemRect = this._element.getBoundingClientRect();
    226          if (state == "hiding" && this._lastState != "hidden") {
    227            // If we are on the hiding transition, and the pointer
    228            // moved near the box, restore to the previous state.
    229            if (event.clientY <= elemRect.bottom + 50) {
    230              this._state = this._lastState;
    231              this._timeoutHide.start();
    232            }
    233          } else if (state == "ontop" || this._lastState != "hidden") {
    234            // State being "ontop" or the previous state not being
    235            // "hidden" indicates this current warning box is shown
    236            // in response to user's action. Hide it immediately when
    237            // the pointer leaves that area.
    238            if (event.clientY > elemRect.bottom + 50) {
    239              this._state = "hidden";
    240              this._timeoutHide.cancel();
    241            }
    242          }
    243        }
    244        break;
    245      }
    246      case "transitionend":
    247      case "transitioncancel": {
    248        if (this._state == "hiding") {
    249          this._doHide();
    250        }
    251        if (this._state == "onscreen") {
    252          window.dispatchEvent(new CustomEvent("FullscreenWarningOnScreen"));
    253        }
    254        break;
    255      }
    256      case "activate": {
    257        this._state = "onscreen";
    258        this._timeoutHide.start();
    259        break;
    260      }
    261      case "deactivate": {
    262        this._state = "hidden";
    263        this._timeoutHide.cancel();
    264        break;
    265      }
    266    }
    267  },
    268 };
    269 
    270 var PointerLock = {
    271  _isActive: false,
    272 
    273  /**
    274   * @returns {boolean} - true if pointer lock is currently active for the
    275   * associated window.
    276   */
    277  get isActive() {
    278    return this._isActive;
    279  },
    280 
    281  entered(originNoSuffix) {
    282    this._isActive = true;
    283    Services.obs.notifyObservers(null, "pointer-lock-entered");
    284    PointerlockFsWarning.showPointerLock(originNoSuffix);
    285  },
    286 
    287  exited() {
    288    this._isActive = false;
    289    PointerlockFsWarning.close("pointerlock-warning");
    290  },
    291 };
    292 
    293 var FullScreen = {
    294  init() {
    295    XPCOMUtils.defineLazyPreferenceGetter(
    296      this,
    297      "permissionsFullScreenAllowed",
    298      "permissions.fullscreen.allowed"
    299    );
    300 
    301    let notificationExitButton = document.getElementById(
    302      "fullscreen-exit-button"
    303    );
    304    notificationExitButton.addEventListener("click", this.exitDomFullScreen);
    305 
    306    // Called when the Firefox window go into fullscreen.
    307    addEventListener("fullscreen", this, true);
    308 
    309    // Called only when fullscreen is requested
    310    // by the parent (eg: via the browser-menu).
    311    // Should not be called when the request comes from
    312    // the content.
    313    addEventListener("willenterfullscreen", this, true);
    314    addEventListener("willexitfullscreen", this, true);
    315    addEventListener("MacFullscreenMenubarRevealUpdate", this, true);
    316 
    317    if (window.fullScreen) {
    318      this.toggle();
    319    }
    320  },
    321 
    322  uninit() {
    323    this.cleanup();
    324  },
    325 
    326  willToggle(aWillEnterFullscreen) {
    327    if (aWillEnterFullscreen) {
    328      document.documentElement.setAttribute("inFullscreen", true);
    329    } else {
    330      document.documentElement.removeAttribute("inFullscreen");
    331    }
    332  },
    333 
    334  get fullScreenToggler() {
    335    delete this.fullScreenToggler;
    336    return (this.fullScreenToggler =
    337      document.getElementById("fullscr-toggler"));
    338  },
    339 
    340  toggle() {
    341    var enterFS = window.fullScreen;
    342 
    343    // Toggle the View:FullScreen command, which controls elements like the
    344    // fullscreen menuitem, and menubars.
    345    let fullscreenCommand = document.getElementById("View:FullScreen");
    346    fullscreenCommand.toggleAttribute("checked", enterFS);
    347 
    348    if (AppConstants.platform == "macosx") {
    349      // Make sure the menu items are adjusted.
    350      document.getElementById("enterFullScreenItem").hidden = enterFS;
    351      document.getElementById("exitFullScreenItem").hidden = !enterFS;
    352      this.shiftMacToolbarDown(0);
    353    }
    354 
    355    let fstoggler = this.fullScreenToggler;
    356    fstoggler.addEventListener("mouseover", this._expandCallback);
    357    fstoggler.addEventListener("dragenter", this._expandCallback);
    358    fstoggler.addEventListener("touchmove", this._expandCallback, {
    359      passive: true,
    360    });
    361 
    362    document.documentElement.toggleAttribute("inFullscreen", enterFS);
    363    document.documentElement.toggleAttribute(
    364      "macOSNativeFullscreen",
    365      enterFS &&
    366        AppConstants.platform == "macosx" &&
    367        (Services.prefs.getBoolPref(
    368          "full-screen-api.macos-native-full-screen"
    369        ) ||
    370          !document.fullscreenElement)
    371    );
    372 
    373    if (!document.fullscreenElement) {
    374      ToolbarIconColor.inferFromText("fullscreen", enterFS);
    375    }
    376 
    377    if (enterFS) {
    378      document.addEventListener("keypress", this._keyToggleCallback);
    379      document.addEventListener("popupshown", this._setPopupOpen);
    380      document.addEventListener("popuphidden", this._setPopupOpen);
    381      gURLBar.controller.addListener(this);
    382 
    383      // In DOM fullscreen mode, we hide toolbars with CSS
    384      if (!document.fullscreenElement) {
    385        this.hideNavToolbox(true);
    386      }
    387    } else {
    388      this.showNavToolbox(false);
    389      // This is needed if they use the context menu to quit fullscreen
    390      this._isPopupOpen = false;
    391      this.cleanup();
    392    }
    393    this._toggleShortcutKeys();
    394  },
    395 
    396  exitDomFullScreen() {
    397    // Don't use `this` here. It does not reliably refer to this object.
    398    if (document.fullscreen) {
    399      document.exitFullscreen();
    400    }
    401  },
    402 
    403  _currentToolbarShift: 0,
    404 
    405  /**
    406   * Shifts the browser toolbar down when it is moused over on macOS in
    407   * fullscreen.
    408   *
    409   * @param {number} shiftSize
    410   *   A distance, in pixels, by which to shift the browser toolbar down.
    411   */
    412  shiftMacToolbarDown(shiftSize) {
    413    if (typeof shiftSize !== "number") {
    414      console.error("Tried to shift the toolbar by a non-numeric distance.");
    415      return;
    416    }
    417 
    418    // shiftSize is sent from Cocoa widget code as a very precise double. We
    419    // don't need that kind of precision in our CSS.
    420    shiftSize = shiftSize.toFixed(2);
    421    gNavToolbox.classList.toggle("fullscreen-with-menubar", shiftSize > 0);
    422 
    423    let transform = shiftSize > 0 ? `translateY(${shiftSize}px)` : "";
    424    gNavToolbox.style.transform = transform;
    425    gURLBar.style.transform = gURLBar.hasAttribute("breakout") ? transform : "";
    426    if (shiftSize > 0) {
    427      // If the mouse tracking missed our fullScreenToggler, then the toolbox
    428      // might not have been shown before the menubar is animated down. Make
    429      // sure it is shown now.
    430      if (!this.fullScreenToggler.hidden) {
    431        this.showNavToolbox();
    432      }
    433    }
    434 
    435    this._currentToolbarShift = shiftSize;
    436  },
    437 
    438  handleEvent(event) {
    439    switch (event.type) {
    440      case "willenterfullscreen":
    441        this.willToggle(true);
    442        break;
    443      case "willexitfullscreen":
    444        this.willToggle(false);
    445        break;
    446      case "fullscreen":
    447        this.toggle();
    448        break;
    449      case "MacFullscreenMenubarRevealUpdate":
    450        this.shiftMacToolbarDown(event.detail);
    451        break;
    452    }
    453  },
    454 
    455  _logWarningPermissionPromptFS(actionStringKey) {
    456    let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
    457      Ci.nsIScriptError
    458    );
    459    let message = gBrowserBundle.GetStringFromName(
    460      `permissions.fullscreen.${actionStringKey}`
    461    );
    462    consoleMsg.initWithWindowID(
    463      message,
    464      gBrowser.currentURI.spec,
    465      0,
    466      0,
    467      Ci.nsIScriptError.warningFlag,
    468      "FullScreen",
    469      gBrowser.selectedBrowser.innerWindowID
    470    );
    471    Services.console.logMessage(consoleMsg);
    472  },
    473 
    474  _handlePermPromptShow() {
    475    if (
    476      !FullScreen.permissionsFullScreenAllowed &&
    477      window.fullScreen &&
    478      PopupNotifications.getNotification(
    479        this._permissionNotificationIDs
    480      ).filter(n => !n.dismissed).length
    481    ) {
    482      this.exitDomFullScreen();
    483      this._logWarningPermissionPromptFS("fullScreenCanceled");
    484    }
    485  },
    486 
    487  enterDomFullscreen(aBrowser, aActor) {
    488    if (!document.fullscreenElement) {
    489      aActor.requestOrigin = null;
    490      return;
    491    }
    492 
    493    // If we have a current pointerlock warning shown then hide it
    494    // before transition.
    495    PointerlockFsWarning.close("pointerlock-warning");
    496 
    497    // If it is a remote browser, send a message to ask the content
    498    // to enter fullscreen state. We don't need to do so if it is an
    499    // in-process browser, since all related document should have
    500    // entered fullscreen state at this point.
    501    // Additionally, in Fission world, we may need to notify the
    502    // frames in the middle (content frames that embbed the oop iframe where
    503    // the element requesting fullscreen lives) to enter fullscreen
    504    // first.
    505    // This should be done before the active tab check below to ensure
    506    // that the content document handles the pending request. Doing so
    507    // before the check is fine since we also check the activeness of
    508    // the requesting document in content-side handling code.
    509    if (this._isRemoteBrowser(aBrowser)) {
    510      // The cached message recipient in actor is used for fullscreen state
    511      // cleanup, we should not use it while entering fullscreen.
    512      let [targetActor, inProcessBC] = this._getNextMsgRecipientActor(
    513        aActor,
    514        false /* aUseCache */
    515      );
    516      if (!targetActor) {
    517        // If there is no appropriate actor to send the message we have
    518        // no way to complete the transition and should abort by exiting
    519        // fullscreen.
    520        this._abortEnterFullscreen(aActor);
    521        return;
    522      }
    523      // Record that the actor is waiting for its child to enter
    524      // fullscreen so that if it dies we can abort.
    525      targetActor.waitingForChildEnterFullscreen = true;
    526      targetActor.sendAsyncMessage("DOMFullscreen:Entered", {
    527        remoteFrameBC: inProcessBC,
    528      });
    529 
    530      if (inProcessBC) {
    531        // We aren't messaging the request origin yet, skip this time.
    532        return;
    533      }
    534    }
    535 
    536    // If we've received a fullscreen notification, we have to ensure that the
    537    // element that's requesting fullscreen belongs to the browser that's currently
    538    // active. If not, we exit fullscreen since the "full-screen document" isn't
    539    // actually visible now.
    540    if (
    541      !aBrowser ||
    542      gBrowser.selectedBrowser != aBrowser ||
    543      // The top-level window has lost focus since the request to enter
    544      // full-screen was made. Cancel full-screen.
    545      Services.focus.activeWindow != window
    546    ) {
    547      this._abortEnterFullscreen(aActor);
    548      return;
    549    }
    550 
    551    // Remove permission prompts when entering full-screen.
    552    if (!FullScreen.permissionsFullScreenAllowed) {
    553      let notifications = PopupNotifications.getNotification(
    554        this._permissionNotificationIDs
    555      ).filter(n => !n.dismissed);
    556      PopupNotifications.remove(notifications, true);
    557      if (notifications.length) {
    558        this._logWarningPermissionPromptFS("promptCanceled");
    559      }
    560    }
    561    document.documentElement.setAttribute("inDOMFullscreen", true);
    562 
    563    XULBrowserWindow.onEnterDOMFullscreen();
    564 
    565    if (gFindBarInitialized) {
    566      gFindBar.close(true);
    567    }
    568 
    569    // Exit DOM full-screen mode when switching to a different tab.
    570    gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
    571 
    572    // Addon installation should be cancelled when entering DOM fullscreen for security and usability reasons.
    573    // Installation prompts in fullscreen can trick the user into installing unwanted addons.
    574    // In fullscreen the notification box does not have a clear visual association with its parent anymore.
    575    if (gXPInstallObserver.removeAllNotifications(aBrowser)) {
    576      // If notifications have been removed, log a warning to the website console
    577      gXPInstallObserver.logWarningFullScreenInstallBlocked();
    578    }
    579 
    580    PopupNotifications.panel.addEventListener(
    581      "popupshowing",
    582      () => this._handlePermPromptShow(),
    583      true
    584    );
    585  },
    586 
    587  cleanup() {
    588    if (!window.fullScreen) {
    589      MousePosTracker.removeListener(this);
    590      document.removeEventListener("keypress", this._keyToggleCallback);
    591      document.removeEventListener("popupshown", this._setPopupOpen);
    592      document.removeEventListener("popuphidden", this._setPopupOpen);
    593      gURLBar.controller.removeListener(this);
    594    }
    595  },
    596 
    597  _toggleShortcutKeys() {
    598    const kEnterKeyIds = [
    599      "key_enterFullScreen",
    600      "key_enterFullScreen_old",
    601      "key_enterFullScreen_compat",
    602    ];
    603    const kExitKeyIds = [
    604      "key_exitFullScreen",
    605      "key_exitFullScreen_old",
    606      "key_exitFullScreen_compat",
    607    ];
    608    for (let id of window.fullScreen ? kEnterKeyIds : kExitKeyIds) {
    609      document.getElementById(id)?.setAttribute("disabled", "true");
    610    }
    611    for (let id of window.fullScreen ? kExitKeyIds : kEnterKeyIds) {
    612      document.getElementById(id)?.removeAttribute("disabled");
    613    }
    614  },
    615 
    616  /**
    617   * Clean up full screen, starting from the request origin's first ancestor
    618   * frame that is OOP.
    619   *
    620   * If there are OOP ancestor frames, we notify the first of those and then bail to
    621   * be called again in that process when it has dealt with the change. This is
    622   * repeated until all ancestor processes have been updated. Once that has happened
    623   * we remove our handlers and attributes and notify the request origin to complete
    624   * the cleanup.
    625   */
    626  cleanupDomFullscreen(aActor) {
    627    let needToWaitForChildExit = false;
    628    // Use the message recipient cached in the actor if possible, especially for
    629    // the case that actor is destroyed, which we are unable to find it by
    630    // walking up the browsing context tree.
    631    let [target, inProcessBC] = this._getNextMsgRecipientActor(
    632      aActor,
    633      true /* aUseCache */
    634    );
    635    if (target) {
    636      needToWaitForChildExit = true;
    637      // Record that the actor is waiting for its child to exit fullscreen so
    638      // that if it dies we can continue cleanup.
    639      target.waitingForChildExitFullscreen = true;
    640      target.sendAsyncMessage("DOMFullscreen:CleanUp", {
    641        remoteFrameBC: inProcessBC,
    642      });
    643      if (inProcessBC) {
    644        return needToWaitForChildExit;
    645      }
    646    }
    647 
    648    PopupNotifications.panel.removeEventListener(
    649      "popupshowing",
    650      () => this._handlePermPromptShow(),
    651      true
    652    );
    653 
    654    PointerlockFsWarning.close("fullscreen-warning");
    655    gBrowser.tabContainer.removeEventListener(
    656      "TabSelect",
    657      this.exitDomFullScreen
    658    );
    659 
    660    document.documentElement.removeAttribute("inDOMFullscreen");
    661 
    662    return needToWaitForChildExit;
    663  },
    664 
    665  _abortEnterFullscreen(aActor) {
    666    // This function is called synchronously in fullscreen change, so
    667    // we have to avoid calling exitFullscreen synchronously here.
    668    //
    669    // This could reject if we're not currently in fullscreen
    670    // so just ignore rejection.
    671    setTimeout(() => document.exitFullscreen().catch(() => {}), 0);
    672    if (aActor.timerId) {
    673      // Cancel the stopwatch for any fullscreen change to avoid
    674      // errors if it is started again.
    675      Glean.fullscreen.change.cancel(aActor.timerId);
    676      aActor.timerId = null;
    677    }
    678  },
    679 
    680  /**
    681   * Search for the first ancestor of aActor that lives in a different process.
    682   * If found, that ancestor actor and the browsing context for its child which
    683   * was in process are returned. Otherwise [request origin, null].
    684   *
    685   * @param {JSWindowActorParent} aActor
    686   *        The actor that called this function.
    687   * @param {bool} aUseCache
    688   *        Use the recipient cached in the aActor if available.
    689   *
    690   * @return {[JSWindowActorParent, BrowsingContext]}
    691   *         The parent actor which should be sent the next msg and the
    692   *         in process browsing context which is its child. Will be
    693   *         [null, null] if there is no OOP parent actor and request origin
    694   *         is unset. [null, null] is also returned if the intended actor or
    695   *         the calling actor has been destroyed or its associated
    696   *         WindowContext is in BFCache.
    697   */
    698  _getNextMsgRecipientActor(aActor, aUseCache) {
    699    // Walk up the cached nextMsgRecipient to find the next available actor if
    700    // any.
    701    if (aUseCache && aActor.nextMsgRecipient) {
    702      let nextMsgRecipient = aActor.nextMsgRecipient;
    703      while (nextMsgRecipient) {
    704        let [actor] = nextMsgRecipient;
    705        if (
    706          !actor.hasBeenDestroyed() &&
    707          actor.windowContext &&
    708          !actor.windowContext.isInBFCache
    709        ) {
    710          return nextMsgRecipient;
    711        }
    712        nextMsgRecipient = actor.nextMsgRecipient;
    713      }
    714    }
    715 
    716    if (aActor.hasBeenDestroyed()) {
    717      return [null, null];
    718    }
    719 
    720    let childBC = aActor.browsingContext;
    721    let parentBC = childBC.parent;
    722 
    723    // Walk up the browsing context tree from aActor's browsing context
    724    // to find the first ancestor browsing context that's in a different process.
    725    while (parentBC) {
    726      if (!childBC.currentWindowGlobal || !parentBC.currentWindowGlobal) {
    727        break;
    728      }
    729      let childPid = childBC.currentWindowGlobal.osPid;
    730      let parentPid = parentBC.currentWindowGlobal.osPid;
    731 
    732      if (childPid == parentPid) {
    733        childBC = parentBC;
    734        parentBC = childBC.parent;
    735      } else {
    736        break;
    737      }
    738    }
    739 
    740    let target = null;
    741    let inProcessBC = null;
    742 
    743    if (parentBC && parentBC.currentWindowGlobal) {
    744      target = parentBC.currentWindowGlobal.getActor("DOMFullscreen");
    745      inProcessBC = childBC;
    746      aActor.nextMsgRecipient = [target, inProcessBC];
    747    } else {
    748      target = aActor.requestOrigin;
    749    }
    750 
    751    if (
    752      !target ||
    753      target.hasBeenDestroyed() ||
    754      target.windowContext?.isInBFCache
    755    ) {
    756      return [null, null];
    757    }
    758    return [target, inProcessBC];
    759  },
    760 
    761  _isRemoteBrowser(aBrowser) {
    762    return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
    763  },
    764 
    765  getMouseTargetRect() {
    766    return this._mouseTargetRect;
    767  },
    768 
    769  // Event callbacks
    770  _expandCallback() {
    771    FullScreen.showNavToolbox();
    772  },
    773 
    774  onMouseEnter() {
    775    this.hideNavToolbox();
    776  },
    777 
    778  _keyToggleCallback(aEvent) {
    779    // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
    780    // should provide a way to collapse them too.
    781    if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
    782      FullScreen.hideNavToolbox();
    783    } else if (aEvent.keyCode == aEvent.DOM_VK_F6) {
    784      // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
    785      FullScreen.showNavToolbox();
    786    }
    787  },
    788 
    789  // Checks whether we are allowed to collapse the chrome
    790  _isPopupOpen: false,
    791  _isChromeCollapsed: false,
    792 
    793  _setPopupOpen(aEvent) {
    794    // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
    795    // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
    796    // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
    797    // toggles chrome when moving mouse to the top, it doesn't go away again.
    798    let target = aEvent.originalTarget;
    799    if (target.localName == "tooltip" || target.id == "tab-preview-panel") {
    800      return;
    801    }
    802    if (
    803      aEvent.type == "popupshown" &&
    804      !FullScreen._isChromeCollapsed &&
    805      target.getAttribute("nopreventnavboxhide") != "true"
    806    ) {
    807      FullScreen._isPopupOpen = true;
    808    } else if (aEvent.type == "popuphidden") {
    809      FullScreen._isPopupOpen = false;
    810      // Try again to hide toolbar when we close the popup.
    811      FullScreen.hideNavToolbox(true);
    812    }
    813  },
    814 
    815  // UrlbarController listener method
    816  onViewOpen() {
    817    if (!this._isChromeCollapsed) {
    818      this._isPopupOpen = true;
    819    }
    820  },
    821 
    822  // UrlbarController listener method
    823  onViewClose() {
    824    this._isPopupOpen = false;
    825    this.hideNavToolbox(true);
    826  },
    827 
    828  get navToolboxHidden() {
    829    return this._isChromeCollapsed;
    830  },
    831 
    832  // Autohide helpers for the context menu item
    833  updateAutohideMenuitem(aItem) {
    834    aItem.toggleAttribute(
    835      "checked",
    836      Services.prefs.getBoolPref("browser.fullscreen.autohide")
    837    );
    838  },
    839  setAutohide() {
    840    Services.prefs.setBoolPref(
    841      "browser.fullscreen.autohide",
    842      !Services.prefs.getBoolPref("browser.fullscreen.autohide")
    843    );
    844    // Try again to hide toolbar when we change the pref.
    845    FullScreen.hideNavToolbox(true);
    846  },
    847 
    848  showNavToolbox(trackMouse = true) {
    849    if (BrowserHandler.kiosk) {
    850      return;
    851    }
    852    this.fullScreenToggler.hidden = true;
    853    gNavToolbox.removeAttribute("fullscreenShouldAnimate");
    854    gNavToolbox.style.marginTop = "";
    855 
    856    if (!this._isChromeCollapsed) {
    857      return;
    858    }
    859 
    860    // Track whether mouse is near the toolbox
    861    if (trackMouse) {
    862      let rect = gBrowser.tabpanels.getBoundingClientRect();
    863      this._mouseTargetRect = {
    864        top: rect.top + 50,
    865        bottom: rect.bottom,
    866        left: rect.left,
    867        right: rect.right,
    868      };
    869      MousePosTracker.addListener(this);
    870    }
    871 
    872    this._isChromeCollapsed = false;
    873    Services.obs.notifyObservers(
    874      gNavToolbox,
    875      "fullscreen-nav-toolbox",
    876      "shown"
    877    );
    878  },
    879 
    880  hideNavToolbox(aAnimate = false) {
    881    if (this._isChromeCollapsed) {
    882      return;
    883    }
    884    if (!Services.prefs.getBoolPref("browser.fullscreen.autohide")) {
    885      return;
    886    }
    887    // a popup menu is open in chrome: don't collapse chrome
    888    if (this._isPopupOpen) {
    889      return;
    890    }
    891 
    892    // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
    893    // unless we are kiosk mode
    894    let focused = document.commandDispatcher.focusedElement;
    895    if (
    896      focused &&
    897      focused.ownerDocument == document &&
    898      focused.localName == "input" &&
    899      !BrowserHandler.kiosk
    900    ) {
    901      // But try collapse the chrome again when anything happens which can make
    902      // it lose the focus. We cannot listen on "blur" event on focused here
    903      // because that event can be triggered by "mousedown", and hiding chrome
    904      // would cause the content to move. This combination may split a single
    905      // click into two actionless halves.
    906      let retryHideNavToolbox = () => {
    907        // Wait for at least a frame to give it a chance to be passed down to
    908        // the content.
    909        requestAnimationFrame(() => {
    910          setTimeout(() => {
    911            // In the meantime, it's possible that we exited fullscreen somehow,
    912            // so only hide the toolbox if we're still in fullscreen mode.
    913            if (window.fullScreen) {
    914              this.hideNavToolbox(aAnimate);
    915            }
    916          }, 0);
    917        });
    918        window.removeEventListener("keydown", retryHideNavToolbox);
    919        window.removeEventListener("click", retryHideNavToolbox);
    920      };
    921      window.addEventListener("keydown", retryHideNavToolbox);
    922      window.addEventListener("click", retryHideNavToolbox);
    923      return;
    924    }
    925 
    926    if (!BrowserHandler.kiosk) {
    927      this.fullScreenToggler.hidden = false;
    928    }
    929 
    930    if (
    931      aAnimate &&
    932      window.matchMedia("(prefers-reduced-motion: no-preference)").matches &&
    933      !BrowserHandler.kiosk
    934    ) {
    935      gNavToolbox.setAttribute("fullscreenShouldAnimate", true);
    936    }
    937 
    938    gNavToolbox.style.marginTop =
    939      -gNavToolbox.getBoundingClientRect().height + "px";
    940    this._isChromeCollapsed = true;
    941    Services.obs.notifyObservers(
    942      gNavToolbox,
    943      "fullscreen-nav-toolbox",
    944      "hidden"
    945    );
    946 
    947    MousePosTracker.removeListener(this);
    948  },
    949 };
    950 
    951 ChromeUtils.defineLazyGetter(FullScreen, "_permissionNotificationIDs", () => {
    952  let { PermissionUI } = ChromeUtils.importESModule(
    953    "resource:///modules/PermissionUI.sys.mjs"
    954  );
    955  return (
    956    Object.values(PermissionUI)
    957      .filter(value => {
    958        let returnValue;
    959        try {
    960          returnValue = value.prototype.notificationID;
    961        } catch (err) {
    962          if (err.message === "Not implemented.") {
    963            returnValue = false;
    964          } else {
    965            throw err;
    966          }
    967        }
    968        return returnValue;
    969      })
    970      .map(value => value.prototype.notificationID)
    971      // Additionally include webRTC permission prompt which does not use PermissionUI
    972      .concat(["webRTC-shareDevices"])
    973  );
    974 });