tor-browser

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

ScreenshotsComponentChild.sys.mjs (12385B)


      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 lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
      9  ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs",
     10 });
     11 
     12 const SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF =
     13  "screenshots.browser.component.preventContentEvents";
     14 
     15 export class ScreenshotsComponentChild extends JSWindowActorChild {
     16  #resizeTask;
     17  #scrollTask;
     18  #overlay;
     19  #preventableEventsAdded = false;
     20 
     21  static OVERLAY_EVENTS = [
     22    "click",
     23    "pointerdown",
     24    "pointermove",
     25    "pointerup",
     26    "keyup",
     27    "keydown",
     28  ];
     29 
     30  // The following events are only listened to so we can prevent them from
     31  // reaching the content page. The events in OVERLAY_EVENTS are also prevented.
     32  static PREVENTABLE_EVENTS = [
     33    "mousemove",
     34    "mousedown",
     35    "mouseup",
     36    "mouseenter",
     37    "mouseover",
     38    "mouseout",
     39    "mouseleave",
     40    "touchstart",
     41    "touchmove",
     42    "touchend",
     43    "dblclick",
     44    "auxclick",
     45    "keypress",
     46    "contextmenu",
     47    "pointerenter",
     48    "pointerover",
     49    "pointerout",
     50    "pointerleave",
     51  ];
     52 
     53  get overlay() {
     54    return this.#overlay;
     55  }
     56 
     57  receiveMessage(message) {
     58    switch (message.name) {
     59      case "Screenshots:ShowOverlay":
     60        return this.startScreenshotsOverlay();
     61      case "Screenshots:HideOverlay":
     62        return this.endScreenshotsOverlay(message.data);
     63      case "Screenshots:isOverlayShowing":
     64        return this.overlay?.initialized;
     65      case "Screenshots:getFullPageBounds":
     66        return this.getFullPageBounds();
     67      case "Screenshots:getVisibleBounds":
     68        return this.getVisibleBounds();
     69      case "Screenshots:getDocumentTitle":
     70        return this.getDocumentTitle();
     71      case "Screenshots:GetMethodsUsed":
     72        return this.getMethodsUsed();
     73      case "Screenshots:RemoveEventListeners":
     74        return this.removeEventListeners();
     75      case "Screenshots:AddEventListeners":
     76        return this.addEventListeners();
     77      case "Screenshots:MoveFocusToContent":
     78        return this.focusOverlay(message.data);
     79      case "Screenshots:ClearFocus":
     80        Services.focus.clearFocus(this.contentWindow);
     81        return null;
     82    }
     83    return null;
     84  }
     85 
     86  handleEvent(event) {
     87    if (!event.isTrusted) {
     88      return;
     89    }
     90 
     91    // Handle overlay events here
     92    if (
     93      [
     94        ...ScreenshotsComponentChild.OVERLAY_EVENTS,
     95        ...ScreenshotsComponentChild.PREVENTABLE_EVENTS,
     96        "selectionchange",
     97      ].includes(event.type)
     98    ) {
     99      if (!this.overlay?.initialized) {
    100        return;
    101      }
    102 
    103      // Preventing a pointerdown event throws an error in debug builds.
    104      // See https://searchfox.org/mozilla-central/rev/b41bb321fe4bd7d03926083698ac498ebec0accf/widget/WidgetEventImpl.cpp#566-572
    105      // Don't prevent the default context menu.
    106      if (!["contextmenu", "pointerdown"].includes(event.type)) {
    107        event.preventDefault();
    108      }
    109 
    110      event.stopImmediatePropagation();
    111      this.overlay.handleEvent(event);
    112      return;
    113    }
    114 
    115    switch (event.type) {
    116      case "beforeunload":
    117        this.requestCancelScreenshot("Navigation");
    118        break;
    119      case "resize":
    120        if (!this.#resizeTask && this.overlay?.initialized) {
    121          this.#resizeTask = new lazy.DeferredTask(() => {
    122            this.overlay.updateScreenshotsOverlayDimensions("resize");
    123          }, 16);
    124        }
    125        this.#resizeTask.arm();
    126        break;
    127      case "scroll":
    128        if (!this.#scrollTask && this.overlay?.initialized) {
    129          this.#scrollTask = new lazy.DeferredTask(() => {
    130            this.overlay.updateScreenshotsOverlayDimensions("scroll");
    131          }, 16);
    132        }
    133        this.#scrollTask.arm();
    134        break;
    135      case "Screenshots:Close":
    136        this.requestCancelScreenshot(event.detail.reason);
    137        break;
    138      case "Screenshots:Copy":
    139        this.requestCopyScreenshot(event.detail.region);
    140        break;
    141      case "Screenshots:Download":
    142        this.requestDownloadScreenshot(event.detail.region);
    143        break;
    144      case "Screenshots:OverlaySelection": {
    145        let { hasSelection, overlayState } = event.detail;
    146        this.sendOverlaySelection({ hasSelection, overlayState });
    147        break;
    148      }
    149      case "Screenshots:RecordEvent": {
    150        let { eventName, args } = event.detail;
    151        Glean.screenshots[eventName].record(args);
    152        break;
    153      }
    154      case "Screenshots:ShowPanel":
    155        this.sendAsyncMessage("Screenshots:ShowPanel");
    156        break;
    157      case "Screenshots:HidePanel":
    158        this.sendAsyncMessage("Screenshots:HidePanel");
    159        break;
    160      case "Screenshots:FocusPanel":
    161        this.sendAsyncMessage("Screenshots:MoveFocusToParent", event.detail);
    162        break;
    163    }
    164  }
    165 
    166  /**
    167   * Send a request to cancel the screenshot to the parent process
    168   */
    169  requestCancelScreenshot(reason) {
    170    this.sendAsyncMessage("Screenshots:CancelScreenshot", {
    171      closeOverlay: false,
    172      reason,
    173    });
    174    this.endScreenshotsOverlay();
    175  }
    176 
    177  /**
    178   * Send a request to copy the screenshots
    179   *
    180   * @param {object} region The region dimensions of the screenshot to be copied
    181   */
    182  requestCopyScreenshot(region) {
    183    region.devicePixelRatio = this.contentWindow.devicePixelRatio;
    184    this.sendAsyncMessage("Screenshots:CopyScreenshot", { region });
    185    this.endScreenshotsOverlay({ doNotResetMethods: true });
    186  }
    187 
    188  /**
    189   * Send a request to download the screenshots
    190   *
    191   * @param {object} region The region dimensions of the screenshot to be downloaded
    192   */
    193  requestDownloadScreenshot(region) {
    194    region.devicePixelRatio = this.contentWindow.devicePixelRatio;
    195    this.sendAsyncMessage("Screenshots:DownloadScreenshot", {
    196      title: this.getDocumentTitle(),
    197      region,
    198    });
    199    this.endScreenshotsOverlay({ doNotResetMethods: true });
    200  }
    201 
    202  getDocumentTitle() {
    203    return this.document.title;
    204  }
    205 
    206  sendOverlaySelection(data) {
    207    this.sendAsyncMessage("Screenshots:OverlaySelection", data);
    208  }
    209 
    210  getMethodsUsed() {
    211    let methodsUsed = this.#overlay.methodsUsed;
    212    this.#overlay.resetMethodsUsed();
    213    return methodsUsed;
    214  }
    215 
    216  focusOverlay(direction) {
    217    this.contentWindow.focus();
    218    this.#overlay.focus(direction);
    219  }
    220 
    221  /**
    222   * Resolves when the document is ready to have an overlay injected into it.
    223   *
    224   * @returns {Promise<boolean>}
    225   *   Resolves to true when document is ready or rejects.
    226   */
    227  documentIsReady() {
    228    const document = this.document;
    229    // Some pages take ages to finish loading - if at all.
    230    // We want to respond to enable the screenshots UI as soon that is possible
    231    function readyEnough() {
    232      return (
    233        document.readyState !== "uninitialized" && document.documentElement
    234      );
    235    }
    236 
    237    if (readyEnough()) {
    238      return Promise.resolve();
    239    }
    240    return new Promise((resolve, reject) => {
    241      function onChange(event) {
    242        if (event.type === "pagehide") {
    243          document.removeEventListener("readystatechange", onChange);
    244          this.contentWindow.removeEventListener("pagehide", onChange);
    245          reject(new Error("document unloaded before it was ready"));
    246        } else if (readyEnough()) {
    247          document.removeEventListener("readystatechange", onChange);
    248          this.contentWindow.removeEventListener("pagehide", onChange);
    249          resolve();
    250        }
    251      }
    252      document.addEventListener("readystatechange", onChange);
    253      this.contentWindow.addEventListener("pagehide", onChange, { once: true });
    254    });
    255  }
    256 
    257  addEventListeners() {
    258    this.contentWindow.addEventListener("beforeunload", this);
    259    this.contentWindow.addEventListener("resize", this);
    260    this.contentWindow.addEventListener("scroll", this);
    261    this.addOverlayEventListeners();
    262  }
    263 
    264  addOverlayEventListeners() {
    265    let chromeEventHandler = this.docShell.chromeEventHandler;
    266    for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
    267      chromeEventHandler.addEventListener(event, this, true);
    268    }
    269 
    270    this.document.addEventListener("selectionchange", this);
    271 
    272    if (Services.prefs.getBoolPref(SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF)) {
    273      for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
    274        chromeEventHandler.addEventListener(event, this, true);
    275      }
    276 
    277      this.#preventableEventsAdded = true;
    278    }
    279  }
    280 
    281  /**
    282   * Wait until the document is ready and then show the screenshots overlay
    283   *
    284   * @returns {boolean} true when document is ready and the overlay is shown
    285   * otherwise false
    286   */
    287  async startScreenshotsOverlay() {
    288    try {
    289      await this.documentIsReady();
    290    } catch (ex) {
    291      console.warn(`ScreenshotsComponentChild: ${ex.message}`);
    292      return false;
    293    }
    294    await this.documentIsReady();
    295    let overlay =
    296      this.overlay ||
    297      (this.#overlay = new lazy.ScreenshotsOverlay(this.document));
    298    this.addEventListeners();
    299 
    300    overlay.initialize();
    301    return true;
    302  }
    303 
    304  removeEventListeners() {
    305    this.contentWindow.removeEventListener("beforeunload", this);
    306    this.contentWindow.removeEventListener("resize", this);
    307    this.contentWindow.removeEventListener("scroll", this);
    308    this.removeOverlayEventListeners();
    309  }
    310 
    311  removeOverlayEventListeners() {
    312    let chromeEventHandler = this.docShell.chromeEventHandler;
    313    for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
    314      chromeEventHandler.removeEventListener(event, this, true);
    315    }
    316 
    317    this.document.removeEventListener("selectionchange", this);
    318 
    319    if (this.#preventableEventsAdded) {
    320      for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
    321        chromeEventHandler.removeEventListener(event, this, true);
    322      }
    323    }
    324 
    325    this.#preventableEventsAdded = false;
    326  }
    327 
    328  /**
    329   * Removes event listeners and the screenshots overlay.
    330   */
    331  endScreenshotsOverlay(options = {}) {
    332    this.removeEventListeners();
    333 
    334    this.overlay?.tearDown(options);
    335    this.#resizeTask?.disarm();
    336    this.#scrollTask?.disarm();
    337  }
    338 
    339  didDestroy() {
    340    this.#resizeTask?.disarm();
    341    this.#scrollTask?.disarm();
    342  }
    343 
    344  /**
    345   * Gets the full page bounds for a full page screenshot.
    346   *
    347   * @returns { object }
    348   *   The device pixel ratio and a DOMRect of the scrollable content bounds.
    349   *
    350   *   devicePixelRatio (float):
    351   *      The device pixel ratio of the screen
    352   *
    353   *   rect (object):
    354   *      top (int):
    355   *        The scroll top position for the content window.
    356   *
    357   *      left (int):
    358   *        The scroll left position for the content window.
    359   *
    360   *      width (int):
    361   *        The scroll width of the content window.
    362   *
    363   *      height (int):
    364   *        The scroll height of the content window.
    365   */
    366  getFullPageBounds() {
    367    let {
    368      scrollMinX,
    369      scrollMinY,
    370      scrollWidth,
    371      scrollHeight,
    372      devicePixelRatio,
    373    } = this.#overlay.windowDimensions.dimensions;
    374    let rect = {
    375      left: scrollMinX,
    376      top: scrollMinY,
    377      right: scrollMinX + scrollWidth,
    378      bottom: scrollMinY + scrollHeight,
    379      width: scrollWidth,
    380      height: scrollHeight,
    381      devicePixelRatio,
    382    };
    383    return rect;
    384  }
    385 
    386  /**
    387   * Gets the visible page bounds for a visible screenshot.
    388   *
    389   * @returns { object }
    390   *   The device pixel ratio and a DOMRect of the current visible
    391   *   content bounds.
    392   *
    393   *   devicePixelRatio (float):
    394   *      The device pixel ratio of the screen
    395   *
    396   *   rect (object):
    397   *      top (int):
    398   *        The top position for the content window.
    399   *
    400   *      left (int):
    401   *        The left position for the content window.
    402   *
    403   *      width (int):
    404   *        The width of the content window.
    405   *
    406   *      height (int):
    407   *        The height of the content window.
    408   */
    409  getVisibleBounds() {
    410    let {
    411      pageScrollX,
    412      pageScrollY,
    413      clientWidth,
    414      clientHeight,
    415      devicePixelRatio,
    416    } = this.#overlay.windowDimensions.dimensions;
    417    let rect = {
    418      left: pageScrollX,
    419      top: pageScrollY,
    420      right: pageScrollX + clientWidth,
    421      bottom: pageScrollY + clientHeight,
    422      width: clientWidth,
    423      height: clientHeight,
    424      devicePixelRatio,
    425    };
    426    return rect;
    427  }
    428 }