Commit 0dfa3fc3 authored by ma1's avatar ma1 Committed by jwilde
Browse files

BB 41631: Prevent weird initial window dimensions caused by subpixel computations

parent ddfd5750
Loading
Loading
Loading
Loading
+138 −50
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
 * You can obtain one at https://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import * as RFPTargetConstants from "resource://gre/modules/RFPTargetConstants.sys.mjs";

const kPrefResistFingerprinting = "privacy.resistFingerprinting";
@@ -41,6 +42,20 @@ function log(...args) {
  lazy.logConsole.log(...args);
}

function forEachWindow(callback) {
  const windowList = Services.wm.getEnumerator("navigator:browser");
  while (windowList.hasMoreElements()) {
    const win = windowList.getNext();
    if (win.gBrowser && !win.closed) {
      try {
        callback(win);
      } catch (e) {
        lazy.logConsole.error(e);
      }
    }
  }
}

class _RFPHelper {
  _resizeObservers = new WeakMap();

@@ -208,9 +223,13 @@ class _RFPHelper {

  _handleResistFingerprintingChanged() {
    this.rfpEnabled = Services.prefs.getBoolPref(kPrefResistFingerprinting);
    if (ChromeUtils.shouldResistFingerprinting("JSLocalePrompt", null)) {
    if (this.rfpEnabled) {
      this._addLanguagePrefObservers();
      Services.ww.registerNotification(this);
      forEachWindow(win => this._attachWindow(win));
    } else {
      forEachWindow(win => this._detachWindow(win));
      Services.ww.unregisterNotification(this);
      this._removeLanguagePrefObservers();
    }
  }
@@ -342,18 +361,12 @@ class _RFPHelper {
  }

  _handleLetterboxingPrefChanged() {
    if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) {
      if (!this._letterboxingPrefObserversAdded) {
        Services.ww.registerNotification(this);
        this._letterboxingPrefObserversAdded = true;
      }
      this._attachAllWindows();
    } else {
      this._detachAllWindows();
      if (this._letterboxingPrefObserversAdded) {
        Services.ww.unregisterNotification(this);
        this._letterboxingPrefObserversAdded = false;
      }
    this.letterboxingEnabled = Services.prefs.getBoolPref(
      kPrefLetterboxing,
      false
    );
    if (this.rfpEnabled) {
      forEachWindow(win => this._updateSizeForTabsInWindow(win));
    }
  }

@@ -435,22 +448,23 @@ class _RFPHelper {
    }
  }

  stepping(aDimension, aIsWidth) {
    if (aDimension <= 500) {
      return 50;
    } else if (aDimension <= 1600) {
      return aIsWidth ? 200 : 100;
    }
    return 200;
  }

  /**
   * Given a width or height, rounds it with the proper stepping.
   */
  steppedSize(aDimension, aIsWidth = false) {
    let stepping;
    if (aDimension <= 50) {
      return aDimension;
    } else if (aDimension <= 500) {
      stepping = 50;
    } else if (aDimension <= 1600) {
      stepping = aIsWidth ? 200 : 100;
    } else {
      stepping = 200;
    }

    return aDimension - (aDimension % stepping);
    return aDimension - (aDimension % this.stepping(aDimension, aIsWidth));
  }

  /**
@@ -477,6 +491,17 @@ class _RFPHelper {
      ])
    );

    const isInitialSize =
      win._rfpOriginalSize &&
      win.outerWidth === win._rfpOriginalSize.width &&
      win.outerHeight === win._rfpOriginalSize.height;

    // We may need to shrink this window to rounded size if the browser container
    // area is taller than the original, meaning extra chrome (like the optional
    // "Only Show on New Tab" bookmarks toobar) was present and now gone.
    const needToShrink =
      isInitialSize && containerHeight > win._rfpOriginalSize.containerHeight;

    log(
      `${logPrefix} contentWidth=${contentWidth} contentHeight=${contentHeight} parentWidth=${parentWidth} parentHeight=${parentHeight} containerWidth=${containerWidth} containerHeight=${containerHeight}${
        isNewTab ? " (new tab)." : "."
@@ -504,6 +529,11 @@ class _RFPHelper {

      log(`${logPrefix} roundDimensions(${aWidth}, ${aHeight})`);

      if (!this.letterboxingEnabled) {
        // just round size to int
        return r(aWidth, aHeight);
      }

      // If the set is empty, we will round the content with the default
      // stepping size.
      if (!this._letterboxingDimensions.length) {
@@ -558,6 +588,17 @@ class _RFPHelper {
              lazy.logConsole.error(e);
            }
          }
          if (needToShrink && win.shrinkToLetterbox()) {
            win.addEventListener(
              "resize",
              () => {
                // We need to record the "new" initial size in this listener
                // because resized dimensions are not immediately available.
                RFPHelper._recordWindowSize(win);
              },
              { once: true }
            );
          }
          if (isTesting) {
            win.promiseDocumentFlushed(() => {
              Services.obs.notifyObservers(
@@ -673,10 +714,41 @@ class _RFPHelper {
    // maximized.
    aWindow.setTimeout(() => {
      tabBrowser.tabbox.classList.add("letterboxing-ready");
      if (!aWindow._rfpOriginalSize) {
        this._recordWindowSize(aWindow);
      }
    });
  }

  _recordWindowSize(aWindow) {
    aWindow.promiseDocumentFlushed(() => {
      aWindow._rfpOriginalSize = {
        width: aWindow.outerWidth,
        height: aWindow.outerHeight,
        containerHeight: aWindow.gBrowser.getBrowserContainer()?.clientHeight,
      };
      log("Recording original window size", aWindow._rfpOriginalSize);
    });
  }

  // We will attach this method to each browser window. When called
  // it will instantly resize the window to exactly fit the selected
  // (possibly letterboxed) browser.
  // Returns true if a window resize will occur, false otherwise.
  shrinkToLetterbox() {
    let { selectedBrowser } = this.gBrowser;
    let stack = selectedBrowser.closest(".browserStack");
    const outer = stack.getBoundingClientRect();
    const inner = selectedBrowser.getBoundingClientRect();
    if (inner.width !== outer.witdh || inner.height !== outer.height) {
      this.resizeBy(inner.width - outer.width, inner.height - outer.height);
      return true;
    }
    return false;
  }

  _attachWindow(aWindow) {
    this._fixRounding(aWindow);
    aWindow.gBrowser.addTabsProgressListener(this);
    aWindow.addEventListener("TabOpen", this);
    let resizeObserver = new aWindow.ResizeObserver(entries => {
@@ -983,20 +1055,6 @@ class _RFPHelper {
    );
  }

  _attachAllWindows() {
    let windowList = Services.wm.getEnumerator("navigator:browser");

    while (windowList.hasMoreElements()) {
      let win = windowList.getNext();

      if (win.closed || !win.gBrowser) {
        continue;
      }

      this._attachWindow(win);
    }
  }

  _detachWindow(aWindow) {
    let resizeObserver = this._resizeObservers.get(aWindow);
    if (resizeObserver) {
@@ -1024,20 +1082,6 @@ class _RFPHelper {
    this._updateLetterboxingColors(aWindow, false);
  }

  _detachAllWindows() {
    let windowList = Services.wm.getEnumerator("navigator:browser");

    while (windowList.hasMoreElements()) {
      let win = windowList.getNext();

      if (win.closed || !win.gBrowser) {
        continue;
      }

      this._detachWindow(win);
    }
  }

  _handleDOMWindowOpened(win) {
    let self = this;

@@ -1069,6 +1113,50 @@ class _RFPHelper {
    this._detachWindow(win);
  }

  _fixRounding(aWindow) {
    if (!this.rfpEnabled) {
      return;
    }

    // tor-browser#43205: in case of subpixels, new windows might have a wrong
    // size because of platform-specific bugs (e.g., Bug 1947439 on Windows).
    const contentContainer = aWindow.document.getElementById("browser");
    const rect = contentContainer.getBoundingClientRect();
    const steppingWidth = this.stepping(rect.width, true);
    const steppingHeight = this.stepping(rect.height, false);
    const deltaWidth =
      rect.width - steppingWidth * Math.round(rect.width / steppingWidth);
    const deltaHeight =
      rect.height - steppingHeight * Math.round(rect.height / steppingHeight);

    // It seems that under X11, a window cannot have all the possible (integer)
    // sizes (see the videos on tor-browser#43205 and Bug 1947439)...
    // We observed this behavior with 1.25 scaling, but we could not find
    // where it happens exactly, so this code might be wrong.
    // On the same system, this problem does not happen with Wayland.
    if (AppConstants.platform === "linux") {
      let targetWidth = aWindow.outerWidth - deltaWidth;
      let targetHeight = aWindow.outerHeight - deltaHeight;
      const x11Size = s =>
        Math.floor(
          // This first rounding is done by Gecko, rather than X11.
          Math.round(s * aWindow.devicePixelRatio) / aWindow.devicePixelRatio
        );
      const x11Width = x11Size(targetWidth);
      const x11Height = x11Size(targetHeight);
      if (x11Width < targetWidth) {
        targetWidth = x11Width + 2;
      }
      if (x11Height < targetHeight) {
        targetHeight = x11Height + 2;
      }
      // resizeTo truncates on X11, so we compensate.
      aWindow.resizeTo(Math.ceil(targetWidth), Math.ceil(targetHeight));
    } else {
      aWindow.resizeBy(deltaWidth, deltaHeight);
    }
  }

  getTargets() {
    return RFPTargetConstants.Targets;
  }