tor-browser

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

DOMFullscreenParent.sys.mjs (11446B)


      1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
      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 export class DOMFullscreenParent extends JSWindowActorParent {
      7  // These properties get set by browser-fullScreenAndPointerLock.js.
      8  // TODO: Bug 1743703 - Consider moving the messaging component of
      9  //       browser-fullScreenAndPointerLock.js into the actor
     10  waitingForChildEnterFullscreen = false;
     11  waitingForChildExitFullscreen = false;
     12  // Cache the next message recipient actor and in-process browsing context that
     13  // is computed by _getNextMsgRecipientActor() of
     14  // browser-fullScreenAndPointerLock.js, this is used to ensure the fullscreen
     15  // cleanup messages goes the same route as fullscreen request, especially for
     16  // the cleanup that happens after actor is destroyed.
     17  // TODO: Bug 1743703 - Consider moving the messaging component of
     18  //       browser-fullScreenAndPointerLock.js into the actor
     19  nextMsgRecipient = null;
     20 
     21  updateFullscreenWindowReference(aWindow) {
     22    if (aWindow.document.documentElement.hasAttribute("inDOMFullscreen")) {
     23      this._fullscreenWindow = aWindow;
     24    } else {
     25      delete this._fullscreenWindow;
     26    }
     27  }
     28 
     29  cleanupDomFullscreen(aWindow) {
     30    if (!aWindow.FullScreen) {
     31      return;
     32    }
     33 
     34    // If we don't need to wait for child reply, i.e. cleanupDomFullscreen
     35    // doesn't message to child, and we've exit the fullscreen, there won't be
     36    // DOMFullscreen:Painted message from child and it is possible that no more
     37    // paint would be triggered, so just notify fullscreen-painted observer.
     38    if (
     39      !aWindow.FullScreen.cleanupDomFullscreen(this) &&
     40      !aWindow.document.fullscreen
     41    ) {
     42      Services.obs.notifyObservers(aWindow, "fullscreen-painted");
     43    }
     44  }
     45 
     46  /**
     47   * Clean up fullscreen state and resume chrome UI if window is in fullscreen
     48   * and this actor is the one where the original fullscreen enter or
     49   * exit request comes.
     50   */
     51  _cleanupFullscreenStateAndResumeChromeUI(aWindow) {
     52    this.cleanupDomFullscreen(aWindow);
     53    if (this.requestOrigin == this && aWindow.document.fullscreen) {
     54      aWindow.windowUtils.remoteFrameFullscreenReverted();
     55    }
     56  }
     57 
     58  didDestroy() {
     59    this._didDestroy = true;
     60 
     61    let window = this._fullscreenWindow;
     62    if (!window) {
     63      let topBrowsingContext = this.browsingContext.top;
     64      let browser = topBrowsingContext.embedderElement;
     65      if (!browser) {
     66        return;
     67      }
     68 
     69      if (
     70        this.waitingForChildExitFullscreen ||
     71        this.waitingForChildEnterFullscreen
     72      ) {
     73        this.waitingForChildExitFullscreen = false;
     74        this.waitingForChildEnterFullscreen = false;
     75        // We were destroyed while waiting for our DOMFullscreenChild to exit
     76        // or enter fullscreen, run cleanup steps anyway.
     77        this._cleanupFullscreenStateAndResumeChromeUI(browser.ownerGlobal);
     78      }
     79 
     80      if (this != this.requestOrigin) {
     81        // The current fullscreen requester should handle the fullsceen event
     82        // if any.
     83        this.removeListeners(browser.ownerGlobal);
     84      }
     85      return;
     86    }
     87 
     88    if (this.waitingForChildEnterFullscreen) {
     89      this.waitingForChildEnterFullscreen = false;
     90      if (window.document.fullscreen) {
     91        // We were destroyed while waiting for our DOMFullscreenChild
     92        // to transition to fullscreen so we abort the entire
     93        // fullscreen transition to prevent getting stuck in a
     94        // partial fullscreen state. We need to go through the
     95        // document since window.Fullscreen could be undefined
     96        // at this point.
     97        //
     98        // This could reject if we're not currently in fullscreen
     99        // so just ignore rejection.
    100        window.document.exitFullscreen().catch(() => {});
    101        return;
    102      }
    103      this.cleanupDomFullscreen(window);
    104    }
    105 
    106    // Need to resume Chrome UI if the window is still in fullscreen UI
    107    // to avoid the window stays in fullscreen problem. (See Bug 1620341)
    108    if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
    109      this.cleanupDomFullscreen(window);
    110      if (window.windowUtils) {
    111        window.windowUtils.remoteFrameFullscreenReverted();
    112      }
    113    } else if (this.waitingForChildExitFullscreen) {
    114      this.waitingForChildExitFullscreen = false;
    115      // We were destroyed while waiting for our DOMFullscreenChild to exit
    116      // run cleanup steps anyway.
    117      this._cleanupFullscreenStateAndResumeChromeUI(window);
    118    }
    119    this.updateFullscreenWindowReference(window);
    120  }
    121 
    122  receiveMessage(aMessage) {
    123    let topBrowsingContext = this.browsingContext.top;
    124    let browser = topBrowsingContext.embedderElement;
    125 
    126    if (!browser) {
    127      // No need to go further when the browser is not accessible anymore
    128      // (which can happen when the tab is closed for instance),
    129      return;
    130    }
    131 
    132    let window = browser.ownerGlobal;
    133    switch (aMessage.name) {
    134      case "DOMFullscreen:Request": {
    135        this.manager.fullscreen = true;
    136        this.waitingForChildExitFullscreen = false;
    137        this.requestOrigin = this;
    138        this.addListeners(window);
    139        window.windowUtils.remoteFrameFullscreenChanged(browser);
    140        break;
    141      }
    142      case "DOMFullscreen:NewOrigin": {
    143        // Don't show the warning if we've already exited fullscreen.
    144        if (window.document.fullscreen) {
    145          window.PointerlockFsWarning.showFullScreen(
    146            aMessage.data.originNoSuffix
    147          );
    148        }
    149        this.updateFullscreenWindowReference(window);
    150        break;
    151      }
    152      case "DOMFullscreen:Entered": {
    153        this.manager.fullscreen = true;
    154        this.nextMsgRecipient = null;
    155        this.waitingForChildEnterFullscreen = false;
    156        window.FullScreen.enterDomFullscreen(browser, this);
    157        this.updateFullscreenWindowReference(window);
    158        break;
    159      }
    160      case "DOMFullscreen:Exit": {
    161        this.manager.fullscreen = false;
    162        this.waitingForChildEnterFullscreen = false;
    163        window.windowUtils.remoteFrameFullscreenReverted();
    164        break;
    165      }
    166      case "DOMFullscreen:Exited": {
    167        this.manager.fullscreen = false;
    168        this.waitingForChildExitFullscreen = false;
    169        this.cleanupDomFullscreen(window);
    170        this.updateFullscreenWindowReference(window);
    171        break;
    172      }
    173      case "DOMFullscreen:Painted": {
    174        this.waitingForChildExitFullscreen = false;
    175        Services.obs.notifyObservers(window, "fullscreen-painted");
    176        this.sendAsyncMessage("DOMFullscreen:Painted", {});
    177        Glean.fullscreen.change.stopAndAccumulate(this.timerId);
    178        this.timerId = null;
    179        break;
    180      }
    181    }
    182  }
    183 
    184  handleEvent(aEvent) {
    185    let window = aEvent.currentTarget.ownerGlobal;
    186    // We can not get the corresponding browsing context from actor if the actor
    187    // has already destroyed, so use event target to get browsing context
    188    // instead.
    189    let requestOrigin = window.browsingContext.fullscreenRequestOrigin?.get();
    190    if (this != requestOrigin) {
    191      // The current fullscreen requester should handle the fullsceen event,
    192      // ignore them if we are not the current requester.
    193      this.removeListeners(window);
    194      return;
    195    }
    196 
    197    switch (aEvent.type) {
    198      case "MozDOMFullscreen:Entered": {
    199        // The event target is the element which requested the DOM
    200        // fullscreen. If we were entering DOM fullscreen for a remote
    201        // browser, the target would be the browser which was the parameter of
    202        // `remoteFrameFullscreenChanged` call. If the fullscreen
    203        // request was initiated from an in-process browser, we need
    204        // to get its corresponding browser here.
    205        let browser;
    206        if (aEvent.target.ownerGlobal == window) {
    207          browser = aEvent.target;
    208        } else {
    209          browser = aEvent.target.ownerGlobal.docShell.chromeEventHandler;
    210        }
    211 
    212        // Addon installation should be cancelled when entering fullscreen for security and usability reasons.
    213        // Installation prompts in fullscreen can trick the user into installing unwanted addons.
    214        // In fullscreen the notification box does not have a clear visual association with its parent anymore.
    215        if (window.gXPInstallObserver) {
    216          window.gXPInstallObserver.removeAllNotifications(browser);
    217        }
    218 
    219        this.timerId = Glean.fullscreen.change.start();
    220        window.FullScreen.enterDomFullscreen(browser, this);
    221        this.updateFullscreenWindowReference(window);
    222 
    223        if (!this.hasBeenDestroyed() && this.requestOrigin) {
    224          window.PointerlockFsWarning.showFullScreen(
    225            this.requestOrigin.manager.documentPrincipal.originNoSuffix
    226          );
    227        }
    228        break;
    229      }
    230      case "MozDOMFullscreen:Exited": {
    231        this.timerId = Glean.fullscreen.change.start();
    232 
    233        // Make sure that the actor has not been destroyed before
    234        // accessing its browsing context. Otherwise, a error may
    235        // occur and hence cleanupDomFullscreen not executed, resulting
    236        // in the browser window being in an unstable state.
    237        // (Bug 1590138).
    238        if (!this.hasBeenDestroyed() && !this.requestOrigin) {
    239          this.requestOrigin = this;
    240        }
    241        this.cleanupDomFullscreen(window);
    242        this.updateFullscreenWindowReference(window);
    243 
    244        // If the document is supposed to be in fullscreen, keep the listener to wait for
    245        // further events.
    246        if (!this.manager.fullscreen) {
    247          this.removeListeners(window);
    248        }
    249        break;
    250      }
    251    }
    252  }
    253 
    254  addListeners(aWindow) {
    255    aWindow.addEventListener(
    256      "MozDOMFullscreen:Entered",
    257      this,
    258      /* useCapture */ true,
    259      /* wantsUntrusted */
    260      false
    261    );
    262    aWindow.addEventListener(
    263      "MozDOMFullscreen:Exited",
    264      this,
    265      /* useCapture */ true,
    266      /* wantsUntrusted */ false
    267    );
    268  }
    269 
    270  removeListeners(aWindow) {
    271    aWindow.removeEventListener("MozDOMFullscreen:Entered", this, true);
    272    aWindow.removeEventListener("MozDOMFullscreen:Exited", this, true);
    273  }
    274 
    275  /**
    276   * Get the actor where the original fullscreen
    277   * enter or exit request comes from.
    278   */
    279  get requestOrigin() {
    280    let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
    281    let requestOrigin = chromeBC?.fullscreenRequestOrigin;
    282    return requestOrigin && requestOrigin.get();
    283  }
    284 
    285  /**
    286   * Store the actor where the original fullscreen
    287   * enter or exit request comes from in the top level
    288   * browsing context.
    289   */
    290  set requestOrigin(aActor) {
    291    let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
    292    if (!chromeBC) {
    293      console.error("not able to get browsingContext for chrome window.");
    294      return;
    295    }
    296 
    297    if (aActor) {
    298      chromeBC.fullscreenRequestOrigin = Cu.getWeakReference(aActor);
    299    } else {
    300      delete chromeBC.fullscreenRequestOrigin;
    301    }
    302  }
    303 
    304  hasBeenDestroyed() {
    305    if (this._didDestroy) {
    306      return true;
    307    }
    308 
    309    // The 'didDestroy' callback is not always getting called.
    310    // So we can't rely on it here. Instead, we will try to access
    311    // the browsing context to judge wether the actor has
    312    // been destroyed or not.
    313    try {
    314      return !this.browsingContext;
    315    } catch {
    316      return true;
    317    }
    318  }
    319 }