tor-browser

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

GeckoViewContentChild.sys.mjs (11312B)


      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 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
      6 
      7 // This needs to match ScreenLength.java
      8 const SCREEN_LENGTH_TYPE_PIXEL = 0;
      9 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1;
     10 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2;
     11 const SCREEN_LENGTH_DOCUMENT_WIDTH = 3;
     12 const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4;
     13 
     14 // This need to match PanZoomController.java
     15 const SCROLL_BEHAVIOR_SMOOTH = 0;
     16 const SCROLL_BEHAVIOR_AUTO = 1;
     17 
     18 const SCREEN_ORIENTATION_PORTRAIT = 0;
     19 const SCREEN_ORIENTATION_LANDSCAPE = 1;
     20 
     21 const lazy = {};
     22 
     23 ChromeUtils.defineESModuleGetters(lazy, {
     24  PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs",
     25  SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
     26 });
     27 
     28 export class GeckoViewContentChild extends GeckoViewActorChild {
     29  constructor() {
     30    super();
     31    this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT;
     32  }
     33 
     34  actorCreated() {
     35    this.pageShow = new Promise(resolve => {
     36      this.receivedPageShow = resolve;
     37    });
     38  }
     39 
     40  toPixels(aLength, aType) {
     41    const { contentWindow } = this;
     42    if (aType === SCREEN_LENGTH_TYPE_PIXEL) {
     43      return aLength;
     44    } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) {
     45      return aLength * contentWindow.visualViewport.width;
     46    } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) {
     47      return aLength * contentWindow.visualViewport.height;
     48    } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) {
     49      return aLength * contentWindow.document.body.scrollWidth;
     50    } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) {
     51      return aLength * contentWindow.document.body.scrollHeight;
     52    }
     53 
     54    return aLength;
     55  }
     56 
     57  toScrollBehavior(aBehavior) {
     58    const { contentWindow } = this;
     59    if (!contentWindow) {
     60      return 0;
     61    }
     62    const { windowUtils } = contentWindow;
     63    if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) {
     64      return windowUtils.SCROLL_MODE_SMOOTH;
     65    } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) {
     66      return windowUtils.SCROLL_MODE_INSTANT;
     67    }
     68    return windowUtils.SCROLL_MODE_SMOOTH;
     69  }
     70 
     71  collectSessionState() {
     72    const { docShell, contentWindow } = this;
     73    const history = lazy.SessionHistory.collect(docShell);
     74    let formdata = SessionStoreUtils.collectFormData(contentWindow);
     75    let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow);
     76 
     77    // Save the current document resolution.
     78    let zoom = 1;
     79    const domWindowUtils = contentWindow.windowUtils;
     80    zoom = domWindowUtils.getResolution();
     81    scrolldata = scrolldata || {};
     82    scrolldata.zoom = {};
     83    scrolldata.zoom.resolution = zoom;
     84 
     85    // Save some data that'll help in adjusting the zoom level
     86    // when restoring in a different screen orientation.
     87    const displaySize = {};
     88    const width = {},
     89      height = {};
     90    domWindowUtils.getDocumentViewerSize(width, height);
     91 
     92    displaySize.width = width.value;
     93    displaySize.height = height.value;
     94 
     95    scrolldata.zoom.displaySize = displaySize;
     96 
     97    formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
     98 
     99    return { history, formdata, scrolldata };
    100  }
    101 
    102  orientation() {
    103    const currentOrientationType = this.contentWindow?.screen.orientation.type;
    104    if (!currentOrientationType) {
    105      // Unfortunately, we don't know current screen orientation.
    106      // Return portrait as default.
    107      return SCREEN_ORIENTATION_PORTRAIT;
    108    }
    109    if (currentOrientationType.startsWith("landscape")) {
    110      return SCREEN_ORIENTATION_LANDSCAPE;
    111    }
    112    return SCREEN_ORIENTATION_PORTRAIT;
    113  }
    114 
    115  receiveMessage(message) {
    116    const { name } = message;
    117    debug`receiveMessage: ${name}`;
    118 
    119    switch (name) {
    120      case "GeckoView:DOMFullscreenEntered": {
    121        const windowUtils = this.contentWindow?.windowUtils;
    122        const actor =
    123          this.contentWindow?.windowGlobalChild?.getActor("ContentDelegate");
    124        if (!windowUtils) {
    125          // If we are not able to enter fullscreen, tell the parent to just
    126          // exit.
    127          actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
    128          break;
    129        }
    130        this.lastOrientation = this.orientation();
    131        let remoteFrameBC = message.data.remoteFrameBC;
    132        if (remoteFrameBC) {
    133          let remoteFrame = remoteFrameBC.embedderElement;
    134          if (!remoteFrame) {
    135            // This could happen when the page navigate away and trigger a
    136            // process switching during fullscreen transition, tell the parent
    137            // to just exit.
    138            actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
    139            break;
    140          }
    141 
    142          windowUtils.remoteFrameFullscreenChanged(remoteFrame);
    143          break;
    144        }
    145 
    146        if (
    147          !windowUtils.handleFullscreenRequests() &&
    148          !this.contentWindow?.document.fullscreenElement
    149        ) {
    150          // If we don't actually have any pending fullscreen request
    151          // to handle, neither we have been in fullscreen, tell the
    152          // parent to just exit.
    153          actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
    154        }
    155        break;
    156      }
    157      case "GeckoView:DOMFullscreenExited": {
    158        // During fullscreen, window size is changed. So don't restore viewport size.
    159        const restoreViewSize = this.orientation() == this.lastOrientation;
    160        this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize);
    161        break;
    162      }
    163      case "GeckoView:ZoomToInput": {
    164        const { contentWindow } = this;
    165        const dwu = contentWindow.windowUtils;
    166 
    167        const zoomToFocusedInput = function () {
    168          if (!dwu.flushApzRepaints()) {
    169            dwu.zoomToFocusedInput();
    170            return;
    171          }
    172          Services.obs.addObserver(function apzFlushDone() {
    173            Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
    174            dwu.zoomToFocusedInput();
    175          }, "apz-repaints-flushed");
    176        };
    177 
    178        zoomToFocusedInput();
    179        break;
    180      }
    181      case "RestoreSessionState": {
    182        this.restoreSessionState(message);
    183        break;
    184      }
    185      case "RestoreHistoryAndNavigate": {
    186        const { history, switchId } = message.data;
    187        if (history) {
    188          lazy.SessionHistory.restore(this.docShell, history);
    189          const historyIndex = history.requestedIndex - 1;
    190          const webNavigation = this.docShell.QueryInterface(
    191            Ci.nsIWebNavigation
    192          );
    193 
    194          if (!switchId) {
    195            // TODO: Bug 1648158 This won't work for Fission or HistoryInParent.
    196            webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry();
    197          } else {
    198            webNavigation.resumeRedirectedLoad(switchId, historyIndex);
    199          }
    200        }
    201        break;
    202      }
    203      case "GeckoView:UpdateInitData": {
    204        // Provide a hook for native code to detect a transfer.
    205        Services.obs.notifyObservers(
    206          this.docShell,
    207          "geckoview-content-global-transferred"
    208        );
    209        break;
    210      }
    211      case "GeckoView:ScrollBy": {
    212        const x = {};
    213        const y = {};
    214        const { contentWindow } = this;
    215        const { widthValue, widthType, heightValue, heightType, behavior } =
    216          message.data;
    217        contentWindow.windowUtils.getVisualViewportOffset(x, y);
    218        contentWindow.windowUtils.scrollToVisual(
    219          x.value + this.toPixels(widthValue, widthType),
    220          y.value + this.toPixels(heightValue, heightType),
    221          contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
    222          this.toScrollBehavior(behavior)
    223        );
    224        break;
    225      }
    226      case "GeckoView:ScrollTo": {
    227        const { contentWindow } = this;
    228        const { widthValue, widthType, heightValue, heightType, behavior } =
    229          message.data;
    230        contentWindow.windowUtils.scrollToVisual(
    231          this.toPixels(widthValue, widthType),
    232          this.toPixels(heightValue, heightType),
    233          contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
    234          this.toScrollBehavior(behavior)
    235        );
    236        break;
    237      }
    238      case "CollectSessionState": {
    239        return this.collectSessionState();
    240      }
    241      case "ContainsFormData": {
    242        return this.containsFormData();
    243      }
    244    }
    245 
    246    return null;
    247  }
    248 
    249  async containsFormData() {
    250    const { contentWindow } = this;
    251    let formdata = SessionStoreUtils.collectFormData(contentWindow);
    252    formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
    253    if (formdata) {
    254      return true;
    255    }
    256    return false;
    257  }
    258 
    259  async restoreSessionState(message) {
    260    // Make sure we showed something before restoring scrolling and form data
    261    await this.pageShow;
    262 
    263    const { contentWindow } = this;
    264    const { formdata, scrolldata } = message.data;
    265 
    266    /**
    267     * Restores frame tree |data|, starting at the given root |frame|. As the
    268     * function recurses into descendant frames it will call cb(frame, data) for
    269     * each frame it encounters, starting with the given root.
    270     */
    271    function restoreFrameTreeData(frame, data, cb) {
    272      // Restore data for the root frame.
    273      // The callback can abort by returning false.
    274      if (cb(frame, data) === false) {
    275        return;
    276      }
    277 
    278      if (!data.hasOwnProperty("children")) {
    279        return;
    280      }
    281 
    282      // Recurse into child frames.
    283      SessionStoreUtils.forEachNonDynamicChildFrame(
    284        frame,
    285        (subframe, index) => {
    286          if (data.children[index]) {
    287            restoreFrameTreeData(subframe, data.children[index], cb);
    288          }
    289        }
    290      );
    291    }
    292 
    293    if (formdata) {
    294      restoreFrameTreeData(contentWindow, formdata, (frame, data) => {
    295        // restore() will return false, and thus abort restoration for the
    296        // current |frame| and its descendants, if |data.url| is given but
    297        // doesn't match the loaded document's URL.
    298        return SessionStoreUtils.restoreFormData(frame.document, data);
    299      });
    300    }
    301 
    302    if (scrolldata) {
    303      restoreFrameTreeData(contentWindow, scrolldata, (frame, data) => {
    304        if (data.scroll) {
    305          SessionStoreUtils.restoreScrollPosition(frame, data);
    306        }
    307      });
    308    }
    309 
    310    if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) {
    311      const utils = contentWindow.windowUtils;
    312      // Restore zoom level.
    313      utils.setRestoreResolution(
    314        scrolldata.zoom.resolution,
    315        scrolldata.zoom.displaySize.width,
    316        scrolldata.zoom.displaySize.height
    317      );
    318    }
    319  }
    320 
    321  // eslint-disable-next-line complexity
    322  handleEvent(aEvent) {
    323    debug`handleEvent: ${aEvent.type}`;
    324 
    325    switch (aEvent.type) {
    326      case "pageshow": {
    327        this.receivedPageShow();
    328        break;
    329      }
    330 
    331      case "mozcaretstatechanged":
    332        if (
    333          aEvent.reason === "presscaret" ||
    334          aEvent.reason === "releasecaret"
    335        ) {
    336          this.sendAsyncMessage("GeckoView:PinOnScreen", {
    337            pinned: aEvent.reason === "presscaret",
    338          });
    339        }
    340        break;
    341    }
    342  }
    343 }
    344 
    345 const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent");