1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
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 file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const kPrefResistFingerprinting = "privacy.resistFingerprinting";
9 const kPrefSpoofEnglish = "privacy.spoof_english";
10 const kTopicHttpOnModifyRequest = "http-on-modify-request";
12 const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing";
13 const kPrefLetterboxingDimensions =
14 "privacy.resistFingerprinting.letterboxing.dimensions";
15 const kPrefLetterboxingTesting =
16 "privacy.resistFingerprinting.letterboxing.testing";
17 const kTopicDOMWindowOpened = "domwindowopened";
22 logConsole = console.createInstance({
24 maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel",
32 // ============================================================================
34 // ============================================================================
36 this._initialized = false;
40 if (this._initialized) {
43 this._initialized = true;
45 // Add unconditional observers
46 Services.prefs.addObserver(kPrefResistFingerprinting, this);
47 Services.prefs.addObserver(kPrefLetterboxing, this);
48 XPCOMUtils.defineLazyPreferenceGetter(
50 "_letterboxingDimensions",
51 kPrefLetterboxingDimensions,
54 this._parseLetterboxingDimensions
56 XPCOMUtils.defineLazyPreferenceGetter(
58 "_isLetterboxingTesting",
59 kPrefLetterboxingTesting,
63 // Add RFP and Letterboxing observers if prefs are enabled
64 this._handleResistFingerprintingChanged();
65 this._handleLetterboxingPrefChanged();
69 if (!this._initialized) {
72 this._initialized = false;
74 // Remove unconditional observers
75 Services.prefs.removeObserver(kPrefResistFingerprinting, this);
76 Services.prefs.removeObserver(kPrefLetterboxing, this);
77 // Remove the RFP observers, swallowing exceptions if they weren't present
78 this._removeRFPObservers();
81 observe(subject, topic, data) {
83 case "nsPref:changed":
84 this._handlePrefChanged(data);
86 case kTopicHttpOnModifyRequest:
87 this._handleHttpOnModifyRequest(subject, data);
89 case kTopicDOMWindowOpened:
90 // We attach to the newly created window by adding tabsProgressListener
91 // and event listener on it. We listen for new tabs being added or
92 // the change of the content principal and apply margins accordingly.
93 this._handleDOMWindowOpened(subject);
100 handleEvent(aMessage) {
101 switch (aMessage.type) {
103 let tab = aMessage.target;
104 this._addOrClearContentMargin(tab.linkedBrowser);
112 _handlePrefChanged(data) {
114 case kPrefResistFingerprinting:
115 this._handleResistFingerprintingChanged();
117 case kPrefSpoofEnglish:
118 this._handleSpoofEnglishChanged();
120 case kPrefLetterboxing:
121 this._handleLetterboxingPrefChanged();
128 contentSizeUpdated(win) {
129 this._updateMarginsForTabsInWindow(win);
132 // ============================================================================
134 // ============================================================================
136 Services.prefs.addObserver(kPrefSpoofEnglish, this);
137 if (this._shouldPromptForLanguagePref()) {
138 Services.obs.addObserver(this, kTopicHttpOnModifyRequest);
142 _removeRFPObservers() {
144 Services.prefs.removeObserver(kPrefSpoofEnglish, this);
149 Services.obs.removeObserver(this, kTopicHttpOnModifyRequest);
155 _handleResistFingerprintingChanged() {
156 if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) {
157 this._addRFPObservers();
159 this._removeRFPObservers();
163 _handleSpoofEnglishChanged() {
164 switch (Services.prefs.getIntPref(kPrefSpoofEnglish)) {
165 case 0: // will prompt
166 // This should only happen when turning privacy.resistFingerprinting off.
167 // Works like disabling accept-language spoofing.
169 case 1: // don't spoof
170 // We don't reset intl.accept_languages. Instead, setting
171 // privacy.spoof_english to 1 allows user to change preferred language
172 // settings through Preferences UI.
175 Services.prefs.setCharPref("intl.accept_languages", "en-US, en");
182 _shouldPromptForLanguagePref() {
184 Services.locale.appLocaleAsBCP47.substr(0, 2) !== "en" &&
185 Services.prefs.getIntPref(kPrefSpoofEnglish) === 0
189 _handleHttpOnModifyRequest(subject) {
190 // If we are loading an HTTP page from content, show the
191 // "request English language web pages?" prompt.
192 let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
194 let notificationCallbacks = httpChannel.notificationCallbacks;
195 if (!notificationCallbacks) {
199 let loadContext = notificationCallbacks.getInterface(Ci.nsILoadContext);
200 if (!loadContext || !loadContext.isContent) {
204 if (!subject.URI.schemeIs("http") && !subject.URI.schemeIs("https")) {
207 // The above QI did not throw, the scheme is http[s], and we know the
208 // load context is content, so we must have a true HTTP request from content.
209 // Stop the observer and display the prompt if another window has
210 // not already done so.
211 Services.obs.removeObserver(this, kTopicHttpOnModifyRequest);
213 if (!this._shouldPromptForLanguagePref()) {
217 this._promptForLanguagePreference();
219 // The Accept-Language header for this request was set when the
220 // channel was created. Reset it to match the value that will be
221 // used for future requests.
222 let val = this._getCurrentAcceptLanguageValue(subject.URI);
224 httpChannel.setRequestHeader("Accept-Language", val, false);
228 _promptForLanguagePreference() {
229 // Display two buttons, both with string titles.
230 const l10n = new Localization(
231 ["toolkit/global/resistFingerPrinting.ftl"],
234 const message = l10n.formatValueSync("privacy-spoof-english");
235 const flags = Services.prompt.STD_YES_NO_BUTTONS;
236 const response = Services.prompt.confirmEx(
248 // Update preferences to reflect their response and to prevent the prompt
249 // from being displayed again.
250 Services.prefs.setIntPref(kPrefSpoofEnglish, response == 0 ? 2 : 1);
253 _getCurrentAcceptLanguageValue(uri) {
254 let channel = Services.io.newChannelFromURI(
256 null, // aLoadingNode
257 Services.scriptSecurityManager.getSystemPrincipal(),
258 null, // aTriggeringPrincipal
259 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
260 Ci.nsIContentPolicy.TYPE_OTHER
264 httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
268 return httpChannel.getRequestHeader("Accept-Language");
271 // ==============================================================================
273 // ============================================================================
275 * We use the TabsProgressListener to catch the change of the content
276 * principal. We would clear the margins around the content viewport if
277 * it is the system principal.
279 onLocationChange(aBrowser) {
280 this._addOrClearContentMargin(aBrowser);
283 _handleLetterboxingPrefChanged() {
284 if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) {
285 Services.ww.registerNotification(this);
286 this._registerActor();
287 this._attachAllWindows();
289 this._unregisterActor();
290 this._detachAllWindows();
291 Services.ww.unregisterNotification(this);
296 ChromeUtils.registerWindowActor("RFPHelper", {
298 esModuleURI: "resource:///actors/RFPHelperParent.sys.mjs",
301 esModuleURI: "resource:///actors/RFPHelperChild.sys.mjs",
311 ChromeUtils.unregisterWindowActor("RFPHelper");
314 // The function to parse the dimension set from the pref value. The pref value
315 // should be formated as 'width1xheight1, width2xheight2, ...'. For
316 // example, '100x100, 200x200, 400x200 ...'.
317 _parseLetterboxingDimensions(aPrefValue) {
318 if (!aPrefValue || !aPrefValue.match(/^(?:\d+x\d+,\s*)*(?:\d+x\d+)$/)) {
321 `Invalid pref value for ${kPrefLetterboxingDimensions}: ${aPrefValue}`
327 return aPrefValue.split(",").map(item => {
328 let sizes = item.split("x").map(size => parseInt(size, 10));
337 _addOrClearContentMargin(aBrowser) {
338 let tab = aBrowser.getTabBrowser().getTabForBrowser(aBrowser);
340 // We won't do anything for lazy browsers.
341 if (!aBrowser.isConnected) {
345 // We should apply no margin around an empty tab or a tab with system
347 if (tab.isEmpty || aBrowser.contentPrincipal.isSystemPrincipal) {
348 this._clearContentViewMargin(aBrowser);
350 this._roundContentView(aBrowser);
355 * Given a width or height, returns the appropriate margin to apply.
357 steppedRange(aDimension) {
359 if (aDimension <= 50) {
361 } else if (aDimension <= 500) {
363 } else if (aDimension <= 1600) {
369 return (aDimension % stepping) / 2;
373 * The function will round the given browser by adding margins around the
376 async _roundContentView(aBrowser) {
377 let logId = Math.random();
378 log("_roundContentView[" + logId + "]");
379 let win = aBrowser.ownerGlobal;
380 let browserContainer = aBrowser
382 .getBrowserContainer(aBrowser);
384 let { contentWidth, contentHeight, containerWidth, containerHeight } =
385 await win.promiseDocumentFlushed(() => {
386 let contentWidth = aBrowser.clientWidth;
387 let contentHeight = aBrowser.clientHeight;
388 let containerWidth = browserContainer.clientWidth;
389 let containerHeight = browserContainer.clientHeight;
391 // If the findbar or devtools are out, we need to subtract their height (plus 1
392 // for the separator) from the container height, because we need to adjust our
393 // letterboxing to account for it; however it is not included in that dimension
394 // (but rather is subtracted from the content height.)
395 let findBar = win.gFindBarInitialized ? win.gFindBar : undefined;
397 findBar && !findBar.hidden ? findBar.clientHeight + 1 : 0;
398 let devtools = browserContainer.getElementsByClassName(
399 "devtools-toolbox-bottom-iframe"
401 let devtoolsOffset = devtools.length ? devtools[0].clientHeight : 0;
407 containerHeight: containerHeight - findBarOffset - devtoolsOffset,
412 "_roundContentView[" +
420 " containerHeight=" +
425 let calcMargins = (aWidth, aHeight) => {
428 "_roundContentView[" +
436 // If the set is empty, we will round the content with the default
438 if (!this._letterboxingDimensions.length) {
440 width: this.steppedRange(aWidth),
441 height: this.steppedRange(aHeight),
444 "_roundContentView[" +
458 let matchingArea = aWidth * aHeight;
459 let minWaste = Number.MAX_SAFE_INTEGER;
460 let targetDimensions = undefined;
462 // Find the desired dimensions which waste the least content area.
463 for (let dim of this._letterboxingDimensions) {
464 // We don't need to consider the dimensions which cannot fit into the
465 // real content size.
466 if (dim.width > aWidth || dim.height > aHeight) {
470 let waste = matchingArea - dim.width * dim.height;
472 if (waste >= 0 && waste < minWaste) {
473 targetDimensions = dim;
478 // If we cannot find any dimensions match to the real content window, this
479 // means the content area is smaller the smallest size in the set. In this
480 // case, we won't apply any margins.
481 if (!targetDimensions) {
488 width: (aWidth - targetDimensions.width) / 2,
489 height: (aHeight - targetDimensions.height) / 2,
494 "_roundContentView[" +
508 // Calculating the margins around the browser element in order to round the
509 // content viewport. We will use a 200x100 stepping if the dimension set
511 let margins = calcMargins(containerWidth, containerHeight);
513 // If the size of the content is already quantized, we do nothing.
514 if (aBrowser.style.margin == `${margins.height}px ${margins.width}px`) {
515 log("_roundContentView[" + logId + "] is_rounded == true");
516 if (this._isLetterboxingTesting) {
518 "_roundContentView[" +
520 "] is_rounded == true test:letterboxing:update-margin-finish"
522 Services.obs.notifyObservers(
524 "test:letterboxing:update-margin-finish"
530 win.requestAnimationFrame(() => {
532 "_roundContentView[" +
534 "] setting margins to " +
539 // One cannot (easily) control the color of a margin unfortunately.
540 // An initial attempt to use a border instead of a margin resulted
541 // in offset event dispatching; so for now we use a colorless margin.
542 aBrowser.style.margin = `${margins.height}px ${margins.width}px`;
546 _clearContentViewMargin(aBrowser) {
547 aBrowser.ownerGlobal.requestAnimationFrame(() => {
548 aBrowser.style.margin = "";
552 _updateMarginsForTabsInWindow(aWindow) {
553 let tabBrowser = aWindow.gBrowser;
555 for (let tab of tabBrowser.tabs) {
556 let browser = tab.linkedBrowser;
557 this._addOrClearContentMargin(browser);
561 _attachWindow(aWindow) {
562 aWindow.gBrowser.addTabsProgressListener(this);
563 aWindow.addEventListener("TabOpen", this);
565 // Rounding the content viewport.
566 this._updateMarginsForTabsInWindow(aWindow);
569 _attachAllWindows() {
570 let windowList = Services.wm.getEnumerator("navigator:browser");
572 while (windowList.hasMoreElements()) {
573 let win = windowList.getNext();
575 if (win.closed || !win.gBrowser) {
579 this._attachWindow(win);
583 _detachWindow(aWindow) {
584 let tabBrowser = aWindow.gBrowser;
585 tabBrowser.removeTabsProgressListener(this);
586 aWindow.removeEventListener("TabOpen", this);
588 // Clear all margins and tooltip for all browsers.
589 for (let tab of tabBrowser.tabs) {
590 let browser = tab.linkedBrowser;
591 this._clearContentViewMargin(browser);
595 _detachAllWindows() {
596 let windowList = Services.wm.getEnumerator("navigator:browser");
598 while (windowList.hasMoreElements()) {
599 let win = windowList.getNext();
601 if (win.closed || !win.gBrowser) {
605 this._detachWindow(win);
609 _handleDOMWindowOpened(win) {
612 win.addEventListener(
615 // We attach to the new window when it has been loaded if the new loaded
616 // window is a browsing window.
618 win.document.documentElement.getAttribute("windowtype") !==
623 self._attachWindow(win);
630 export let RFPHelper = new _RFPHelper();