| ... | ... | @@ -22,16 +22,110 @@ const kPrefLetterboxingTesting = | 
| 22 | 22 |    "privacy.resistFingerprinting.letterboxing.testing";
 | 
| 23 | 23 |  const kTopicDOMWindowOpened = "domwindowopened";
 | 
| 24 | 24 |  
 | 
| 25 |  | -var logConsole;
 | 
| 26 |  | -function log(msg) {
 | 
| 27 |  | -  if (!logConsole) {
 | 
| 28 |  | -    logConsole = console.createInstance({
 | 
| 29 |  | -      prefix: "RFPHelper.jsm",
 | 
| 30 |  | -      maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel",
 | 
| 31 |  | -    });
 | 
|  | 25 | +const kPrefResizeWarnings = "privacy.resistFingerprinting.resizeWarnings";
 | 
|  | 26 | +
 | 
|  | 27 | +XPCOMUtils.defineLazyGetter(this, "logConsole", () =>
 | 
|  | 28 | +  console.createInstance({
 | 
|  | 29 | +    prefix: "RFPHelper.jsm",
 | 
|  | 30 | +    maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel",
 | 
|  | 31 | +  })
 | 
|  | 32 | +);
 | 
|  | 33 | +
 | 
|  | 34 | +function log(...args) {
 | 
|  | 35 | +  logConsole.log(...args);
 | 
|  | 36 | +}
 | 
|  | 37 | +
 | 
|  | 38 | +function forEachWindow(callback) {
 | 
|  | 39 | +  const windowList = Services.wm.getEnumerator("navigator:browser");
 | 
|  | 40 | +  while (windowList.hasMoreElements()) {
 | 
|  | 41 | +    const win = windowList.getNext();
 | 
|  | 42 | +    if (win.gBrowser && !win.closed) {
 | 
|  | 43 | +      try {
 | 
|  | 44 | +        callback(win);
 | 
|  | 45 | +      } catch (e) {
 | 
|  | 46 | +        logConsole.error(e);
 | 
|  | 47 | +      }
 | 
|  | 48 | +    }
 | 
|  | 49 | +  }
 | 
|  | 50 | +}
 | 
|  | 51 | +
 | 
|  | 52 | +
 | 
|  | 53 | +async function windowResizeHandler(aEvent) {
 | 
|  | 54 | +  if (RFPHelper.letterboxingEnabled) {
 | 
|  | 55 | +    return;
 | 
|  | 56 | +  }
 | 
|  | 57 | +  if (Services.prefs.getIntPref(kPrefResizeWarnings, 3) <= 0) {
 | 
|  | 58 | +    return;
 | 
|  | 59 | +  }
 | 
|  | 60 | +
 | 
|  | 61 | +  const window = aEvent.currentTarget;
 | 
|  | 62 | +
 | 
|  | 63 | +  // Wait for end of execution queue to ensure we have correct windowState.
 | 
|  | 64 | +  await new Promise(resolve => window.setTimeout(resolve, 0));
 | 
|  | 65 | +  switch (window.windowState) {
 | 
|  | 66 | +    case window.STATE_MAXIMIZED:
 | 
|  | 67 | +    case window.STATE_FULLSCREEN:
 | 
|  | 68 | +      break;
 | 
|  | 69 | +    default:
 | 
|  | 70 | +      return;
 | 
|  | 71 | +  }
 | 
|  | 72 | +
 | 
|  | 73 | +  // Do not add another notification if one is already showing.
 | 
|  | 74 | +  const kNotificationName = "rfp-window-resize-notification";
 | 
|  | 75 | +  let box = window.gNotificationBox;
 | 
|  | 76 | +  if (box.getNotificationWithValue(kNotificationName)) {
 | 
|  | 77 | +    return;
 | 
| 32 | 78 |    }
 | 
| 33 | 79 |  
 | 
| 34 |  | -  logConsole.log(msg);
 | 
|  | 80 | +  // Rate-limit showing our notification if needed.
 | 
|  | 81 | +  if (Date.now() - (windowResizeHandler.timestamp || 0) < 1000) {
 | 
|  | 82 | +    return;
 | 
|  | 83 | +  }
 | 
|  | 84 | +  windowResizeHandler.timestamp = Date.now();
 | 
|  | 85 | +
 | 
|  | 86 | +  const decreaseWarningsCount = () => {
 | 
|  | 87 | +    const currentCount = Services.prefs.getIntPref(kPrefResizeWarnings);
 | 
|  | 88 | +    if (currentCount > 0) {
 | 
|  | 89 | +      Services.prefs.setIntPref(kPrefResizeWarnings, currentCount - 1);
 | 
|  | 90 | +    }
 | 
|  | 91 | +  };
 | 
|  | 92 | +
 | 
|  | 93 | +  const [label, accessKey] = await window.document.l10n.formatValues([
 | 
|  | 94 | +    { id: "basebrowser-rfp-restore-window-size-button-label" },
 | 
|  | 95 | +    { id: "basebrowser-rfp-restore-window-size-button-ak" },
 | 
|  | 96 | +  ]);
 | 
|  | 97 | +
 | 
|  | 98 | +  const buttons = [
 | 
|  | 99 | +    {
 | 
|  | 100 | +      label,
 | 
|  | 101 | +      accessKey,
 | 
|  | 102 | +      popup: null,
 | 
|  | 103 | +      callback() {
 | 
|  | 104 | +        // reset notification timer to work-around resize race conditions
 | 
|  | 105 | +        windowResizeHandler.timestamp = Date.now();
 | 
|  | 106 | +        // restore the original (rounded) size we had stored on window startup
 | 
|  | 107 | +        let { _rfpOriginalSize } = window;
 | 
|  | 108 | +        window.setTimeout(() => {
 | 
|  | 109 | +          window.resizeTo(_rfpOriginalSize.width, _rfpOriginalSize.height);
 | 
|  | 110 | +        }, 0);
 | 
|  | 111 | +      },
 | 
|  | 112 | +    },
 | 
|  | 113 | +  ];
 | 
|  | 114 | +
 | 
|  | 115 | +  box.appendNotification(
 | 
|  | 116 | +    kNotificationName,
 | 
|  | 117 | +    {
 | 
|  | 118 | +      label: { "l10n-id": "basebrowser-rfp-maximize-warning-message" },
 | 
|  | 119 | +      priority: box.PRIORITY_WARNING_LOW,
 | 
|  | 120 | +      eventCallback(event) {
 | 
|  | 121 | +        if (event === "dismissed") {
 | 
|  | 122 | +          // user manually dismissed the notification
 | 
|  | 123 | +          decreaseWarningsCount();
 | 
|  | 124 | +        }
 | 
|  | 125 | +      },
 | 
|  | 126 | +    },
 | 
|  | 127 | +    buttons
 | 
|  | 128 | +  );
 | 
| 35 | 129 |  }
 | 
| 36 | 130 |  
 | 
| 37 | 131 |  class _RFPHelper {
 | 
| ... | ... | @@ -158,7 +252,11 @@ class _RFPHelper { | 
| 158 | 252 |    _handleResistFingerprintingChanged() {
 | 
| 159 | 253 |      if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) {
 | 
| 160 | 254 |        this._addRFPObservers();
 | 
|  | 255 | +      Services.ww.registerNotification(this);
 | 
|  | 256 | +      forEachWindow(win => this._attachWindow(win));
 | 
| 161 | 257 |      } else {
 | 
|  | 258 | +      forEachWindow(win => this._detachWindow(win));
 | 
|  | 259 | +      Services.ww.unregisterNotification(this);
 | 
| 162 | 260 |        this._removeRFPObservers();
 | 
| 163 | 261 |      }
 | 
| 164 | 262 |    }
 | 
| ... | ... | @@ -295,13 +393,11 @@ class _RFPHelper { | 
| 295 | 393 |    }
 | 
| 296 | 394 |  
 | 
| 297 | 395 |    _handleLetterboxingPrefChanged() {
 | 
| 298 |  | -    if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) {
 | 
| 299 |  | -      Services.ww.registerNotification(this);
 | 
| 300 |  | -      this._attachAllWindows();
 | 
| 301 |  | -    } else {
 | 
| 302 |  | -      this._detachAllWindows();
 | 
| 303 |  | -      Services.ww.unregisterNotification(this);
 | 
| 304 |  | -    }
 | 
|  | 396 | +    this.letterboxingEnabled = Services.prefs.getBoolPref(
 | 
|  | 397 | +      kPrefLetterboxing,
 | 
|  | 398 | +      false
 | 
|  | 399 | +    );
 | 
|  | 400 | +    forEachWindow(win => this._updateSizeForTabsInWindow(win));
 | 
| 305 | 401 |    }
 | 
| 306 | 402 |  
 | 
| 307 | 403 |    // The function to parse the dimension set from the pref value. The pref value
 | 
| ... | ... | @@ -402,11 +498,13 @@ class _RFPHelper { | 
| 402 | 498 |      let logPrefix = `_roundContentSize[${Math.random()}]`;
 | 
| 403 | 499 |      log(logPrefix);
 | 
| 404 | 500 |      let win = aBrowser.ownerGlobal;
 | 
|  | 501 | +
 | 
| 405 | 502 |      let browserContainer = aBrowser
 | 
| 406 | 503 |        .getTabBrowser()
 | 
| 407 | 504 |        .getBrowserContainer(aBrowser);
 | 
| 408 | 505 |      let browserParent = aBrowser.parentElement;
 | 
| 409 | 506 |      browserParent.classList.remove("exclude-letterboxing");
 | 
|  | 507 | +
 | 
| 410 | 508 |      let [
 | 
| 411 | 509 |        [contentWidth, contentHeight],
 | 
| 412 | 510 |        [parentWidth, parentHeight],
 | 
| ... | ... | @@ -419,6 +517,27 @@ class _RFPHelper { | 
| 419 | 517 |        ])
 | 
| 420 | 518 |      );
 | 
| 421 | 519 |  
 | 
|  | 520 | +    if (
 | 
|  | 521 | +      !win._rfpSizeOffset ||
 | 
|  | 522 | +      (win._rfpOriginalSize &&
 | 
|  | 523 | +        win.outerWidth === win._rfpOriginalSize.width &&
 | 
|  | 524 | +        win.outerHeight === win._rfpOriginalSize.height)
 | 
|  | 525 | +    ) {
 | 
|  | 526 | +      const BASELINE_ROUNDING = 10;
 | 
|  | 527 | +      const offset = s =>
 | 
|  | 528 | +        s - Math.round(s / BASELINE_ROUNDING) * BASELINE_ROUNDING;
 | 
|  | 529 | +
 | 
|  | 530 | +      win._rfpSizeOffset = {
 | 
|  | 531 | +        width: offset(parentWidth),
 | 
|  | 532 | +        height: offset(parentHeight),
 | 
|  | 533 | +      };
 | 
|  | 534 | +      log(
 | 
|  | 535 | +        `${logPrefix} Window size offsets %o (from %s, %s)`,
 | 
|  | 536 | +        win._rfpSizeOffset,
 | 
|  | 537 | +        parentWidth,
 | 
|  | 538 | +        parentHeight
 | 
|  | 539 | +      );
 | 
|  | 540 | +    }
 | 
| 422 | 541 |      log(
 | 
| 423 | 542 |        `${logPrefix} contentWidth=${contentWidth} contentHeight=${contentHeight} parentWidth=${parentWidth} parentHeight=${parentHeight} containerWidth=${containerWidth} containerHeight=${containerHeight}${
 | 
| 424 | 543 |          isNewTab ? " (new tab)." : "."
 | 
| ... | ... | @@ -437,6 +556,16 @@ class _RFPHelper { | 
| 437 | 556 |        });
 | 
| 438 | 557 |  
 | 
| 439 | 558 |        let result;
 | 
|  | 559 | +
 | 
|  | 560 | +      if (!this.letterboxingEnabled) {
 | 
|  | 561 | +        const offset = win._rfpSizeOffset;
 | 
|  | 562 | +        result = r(aWidth - offset.width, aHeight - offset.height);
 | 
|  | 563 | +        log(
 | 
|  | 564 | +          `${logPrefix} Letterboxing disabled, applying baseline rounding offsets: (${aWidth}, ${aHeight}) => ${result.width} x ${result.height})`
 | 
|  | 565 | +        );
 | 
|  | 566 | +        return result;
 | 
|  | 567 | +      }
 | 
|  | 568 | +
 | 
| 440 | 569 |        log(`${logPrefix} roundDimensions(${aWidth}, ${aHeight})`);
 | 
| 441 | 570 |        // If the set is empty, we will round the content with the default
 | 
| 442 | 571 |        // stepping size.
 | 
| ... | ... | @@ -554,7 +683,6 @@ class _RFPHelper { | 
| 554 | 683 |  
 | 
| 555 | 684 |    _updateSizeForTabsInWindow(aWindow) {
 | 
| 556 | 685 |      let tabBrowser = aWindow.gBrowser;
 | 
| 557 |  | -
 | 
| 558 | 686 |      tabBrowser.tabpanels?.classList.add("letterboxing");
 | 
| 559 | 687 |  
 | 
| 560 | 688 |      for (let tab of tabBrowser.tabs) {
 | 
| ... | ... | @@ -564,10 +692,18 @@ class _RFPHelper { | 
| 564 | 692 |      // we need to add this class late because otherwise new windows get maximized
 | 
| 565 | 693 |      aWindow.setTimeout(() => {
 | 
| 566 | 694 |        tabBrowser.tabpanels?.classList.add("letterboxing-ready");
 | 
|  | 695 | +      if (!aWindow._rfpOriginalSize) {
 | 
|  | 696 | +        aWindow._rfpOriginalSize = {
 | 
|  | 697 | +          width: aWindow.outerWidth,
 | 
|  | 698 | +          height: aWindow.outerHeight,
 | 
|  | 699 | +        };
 | 
|  | 700 | +        log("Recording original window size", aWindow._rfpOriginalSize);
 | 
|  | 701 | +      }
 | 
| 567 | 702 |      });
 | 
| 568 | 703 |    }
 | 
| 569 | 704 |  
 | 
| 570 | 705 |    _attachWindow(aWindow) {
 | 
|  | 706 | +    aWindow.addEventListener("sizemodechange", windowResizeHandler);
 | 
| 571 | 707 |      aWindow.gBrowser.addTabsProgressListener(this);
 | 
| 572 | 708 |      aWindow.addEventListener("TabOpen", this);
 | 
| 573 | 709 |      const resizeObserver = (aWindow._rfpResizeObserver = new aWindow.ResizeObserver(
 | 
| ... | ... | @@ -585,19 +721,7 @@ class _RFPHelper { | 
| 585 | 721 |      this._updateSizeForTabsInWindow(aWindow);
 | 
| 586 | 722 |    }
 | 
| 587 | 723 |  
 | 
| 588 |  | -  _attachAllWindows() {
 | 
| 589 |  | -    let windowList = Services.wm.getEnumerator("navigator:browser");
 | 
| 590 | 724 |  
 | 
| 591 |  | -    while (windowList.hasMoreElements()) {
 | 
| 592 |  | -      let win = windowList.getNext();
 | 
| 593 |  | -
 | 
| 594 |  | -      if (win.closed || !win.gBrowser) {
 | 
| 595 |  | -        continue;
 | 
| 596 |  | -      }
 | 
| 597 |  | -
 | 
| 598 |  | -      this._attachWindow(win);
 | 
| 599 |  | -    }
 | 
| 600 |  | -  }
 | 
| 601 | 725 |  
 | 
| 602 | 726 |    _detachWindow(aWindow) {
 | 
| 603 | 727 |      let tabBrowser = aWindow.gBrowser;
 | 
| ... | ... | @@ -614,20 +738,7 @@ class _RFPHelper { | 
| 614 | 738 |        let browser = tab.linkedBrowser;
 | 
| 615 | 739 |        this._resetContentSize(browser);
 | 
| 616 | 740 |      }
 | 
| 617 |  | -  }
 | 
| 618 |  | -
 | 
| 619 |  | -  _detachAllWindows() {
 | 
| 620 |  | -    let windowList = Services.wm.getEnumerator("navigator:browser");
 | 
| 621 |  | -
 | 
| 622 |  | -    while (windowList.hasMoreElements()) {
 | 
| 623 |  | -      let win = windowList.getNext();
 | 
| 624 |  | -
 | 
| 625 |  | -      if (win.closed || !win.gBrowser) {
 | 
| 626 |  | -        continue;
 | 
| 627 |  | -      }
 | 
| 628 |  | -
 | 
| 629 |  | -      this._detachWindow(win);
 | 
| 630 |  | -    }
 | 
|  | 741 | +    aWindow.removeEventListener("sizemodechange", windowResizeHandler);
 | 
| 631 | 742 |    }
 | 
| 632 | 743 |  
 | 
| 633 | 744 |    _handleDOMWindowOpened(win) {
 |