diff --git a/browser/actors/moz.build b/browser/actors/moz.build index 00d1d87867a0..e84a2a8e7af1 100644 --- a/browser/actors/moz.build +++ b/browser/actors/moz.build @@ -42,7 +42,6 @@ FINAL_TARGET_FILES.actors += [ 'PageInfoChild.jsm', 'PageStyleChild.jsm', 'PluginChild.jsm', - 'RFPHelperChild.jsm', 'SearchTelemetryChild.jsm', 'URIFixupChild.jsm', 'WebRTCChild.jsm', diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 108a8ee961b4..84074bf49cdd 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -28,6 +28,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { E10SUtils: "resource://gre/modules/E10SUtils.jsm", ExtensionsUI: "resource:///modules/ExtensionsUI.jsm", FormValidationHandler: "resource:///modules/FormValidationHandler.jsm", + LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm", HomePage: "resource:///modules/HomePage.jsm", LightweightThemeConsumer: "resource://gre/modules/LightweightThemeConsumer.jsm", LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm", @@ -49,7 +50,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm", PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", ReaderParent: "resource:///modules/ReaderParent.jsm", - RFPHelper: "resource://gre/modules/RFPHelper.jsm", SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm", Sanitizer: "resource:///modules/Sanitizer.jsm", SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm", @@ -1948,6 +1948,8 @@ var gBrowserInit = { ToolbarKeyboardNavigator.uninit(); } + LanguagePrompt.uninit(); + BrowserSearch.uninit(); // Now either cancel delayedStartup, or clean up the services initialized from diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index a30a79935d32..00426c5862ba 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -244,16 +244,6 @@ let ACTORS = { }, }, - RFPHelper: { - child: { - module: "resource:///actors/RFPHelperChild.jsm", - group: "browsers", - events: { - "resize": {}, - }, - }, - }, - SearchTelemetry: { child: { module: "resource:///actors/SearchTelemetryChild.jsm", @@ -425,7 +415,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm", RemoteSettings: "resource://services-settings/remote-settings.js", - RFPHelper: "resource://gre/modules/RFPHelper.jsm", SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm", Sanitizer: "resource:///modules/Sanitizer.jsm", SaveToPocket: "chrome://pocket/content/SaveToPocket.jsm", @@ -1472,7 +1461,6 @@ BrowserGlue.prototype = { DateTimePickerParent.uninit(); Normandy.uninit(); - RFPHelper.uninit(); }, // Set up a listener to enable/disable the screenshots extension @@ -1686,7 +1674,7 @@ BrowserGlue.prototype = { } Services.tm.idleDispatchToMainThread(() => { - RFPHelper.init(); + LanguagePrompt.init(); }); Services.tm.idleDispatchToMainThread(() => { diff --git a/browser/components/resistfingerprinting/test/browser/browser.ini b/browser/components/resistfingerprinting/test/browser/browser.ini index 1aa918b4574b..024ee29907b4 100644 --- a/browser/components/resistfingerprinting/test/browser/browser.ini +++ b/browser/components/resistfingerprinting/test/browser/browser.ini @@ -11,7 +11,6 @@ support-files = head.js [browser_block_mozAddonManager.js] -[browser_dynamical_window_rounding.js] [browser_navigator.js] [browser_netInfo.js] [browser_performanceAPI.js] diff --git a/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js b/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js index ee77494e1816..b11ea22631b7 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js +++ b/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js @@ -25,8 +25,6 @@ add_task(async function() { isnot(tab3Zoom, tab1Zoom, "privacy.resistFingerprinting is true, site-specific zoom level should be disabled"); - await FullZoom.reset(); - BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); BrowserTestUtils.removeTab(tab3); diff --git a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js deleted file mode 100644 index ae8055a49ce5..000000000000 --- a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js +++ /dev/null @@ -1,281 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Bug 1407366 - A test case for reassuring the size of the content viewport is - * rounded if the window is resized when letterboxing is enabled. - * - * A helpful note: if this test starts randomly failing; it may be because the - * zoom level was not reset by an earlier-run test. See Bug 1407366 for an - * example. - */ - -const TEST_PATH = "http://example.net/browser/browser/components/resistfingerprinting/test/browser/"; - -const DEFAULT_ROUNDED_WIDTH_STEP = 200; -const DEFAULT_ROUNDED_HEIGHT_STEP = 100; - -// A set of test cases which defines the width and the height of the outer window. -const TEST_CASES = [ - {width: 1250, height: 1000}, - {width: 1500, height: 1050}, - {width: 1120, height: 760}, - {width: 800, height: 600}, - {width: 640, height: 400}, - {width: 500, height: 350}, - {width: 300, height: 170}, -]; - -function getPlatform() { - const {OS} = Services.appinfo; - if (OS == "WINNT") { - return "win"; - } else if (OS == "Darwin") { - return "mac"; - } - return "linux"; -} - -function handleOSFuzziness(aContent, aTarget) { - /* - * On Windows, we observed off-by-one pixel differences that - * couldn't be expained. When manually setting the window size - * to try to reproduce it; it did not occur. - */ - if (getPlatform() == "win") { - return Math.abs(aContent - aTarget) <= 1; - } - return aContent == aTarget; -} - -function checkForDefaultSetting( - aContentWidth, aContentHeight, aRealWidth, aRealHeight) { - // The default behavior for rounding is to round window with 200x100 stepping. - // So, we can get the rounded size by subtracting the remainder. - let targetWidth = aRealWidth - (aRealWidth % DEFAULT_ROUNDED_WIDTH_STEP); - let targetHeight = aRealHeight - (aRealHeight % DEFAULT_ROUNDED_HEIGHT_STEP); - - // This platform-specific code is explained in the large comment below. - if (getPlatform() != "linux") { - ok(handleOSFuzziness(aContentWidth, targetWidth), - `Default Dimensions: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetWidth}px`); - - ok(handleOSFuzziness(aContentHeight, targetHeight), - `Default Dimensions: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetHeight}px`); - - // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends - return true; - } - // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds. - return handleOSFuzziness(aContentWidth, targetWidth) && handleOSFuzziness(aContentHeight, targetHeight); -} - -async function test_dynamical_window_rounding(aWindow, aCheckFunc) { - // We need to wait for the updating the margins for the newly opened tab, or - // it will affect the following tests. - let promiseForTheFirstRounding = - TestUtils.topicObserved("test:letterboxing:update-margin-finish"); - - info("Open a content tab for testing."); - let tab = await BrowserTestUtils.openNewForegroundTab( - aWindow.gBrowser, TEST_PATH + "file_dummy.html"); - - info("Wait until the margins are applied for the opened tab."); - await promiseForTheFirstRounding; - - let getContainerSize = (aTab) => { - let browserContainer = aWindow.gBrowser - .getBrowserContainer(aTab.linkedBrowser); - return { - containerWidth: browserContainer.clientWidth, - containerHeight: browserContainer.clientHeight, - }; - }; - - for (let {width, height} of TEST_CASES) { - let caseString = "Case " + width + "x" + height + ": "; - // Create a promise for waiting for the margin update. - let promiseRounding = - TestUtils.topicObserved("test:letterboxing:update-margin-finish"); - - let {containerWidth, containerHeight} = getContainerSize(tab); - - info(caseString + "Resize the window and wait until resize event happened (currently " + - containerWidth + "x" + containerHeight + ")"); - await new Promise(resolve => { - ({containerWidth, containerHeight} = getContainerSize(tab)); - info(caseString + "Resizing (currently " + containerWidth + "x" + containerHeight + ")"); - - aWindow.onresize = () => { - ({containerWidth, containerHeight} = getContainerSize(tab)); - info(caseString + "Resized (currently " + containerWidth + "x" + containerHeight + ")"); - if (getPlatform() == "linux" && containerWidth != width) { - /* - * We observed frequent test failures that resulted from receiving an onresize - * event where the browser was resized to an earlier requested dimension. This - * resize event happens on Linux only, and is an artifact of the asynchronous - * resizing. (See more discussion on 1407366#53) - * - * We cope with this problem in two ways. - * - * 1: If we detect that the browser was resized to the wrong value; we - * redo the resize. (This is the lines of code immediately following this - * comment) - * 2: We repeat the test until it works using waitForCondition(). But we still - * test Win/Mac more thoroughly: they do not loop in waitForCondition more - * than once, and can fail the test on the first attempt (because their - * check() functions use ok() while on Linux, we do not all ok() and instead - * rely on waitForCondition to fail). - * - * The logging statements in this test, and RFPHelper.jsm, help narrow down and - * illustrate the issue. - */ - info(caseString + "We hit the weird resize bug. Resize it again."); - aWindow.resizeTo(width, height); - } else { - resolve(); - } - }; - aWindow.resizeTo(width, height); - }); - - ({containerWidth, containerHeight} = getContainerSize(tab)); - info(caseString + "Waiting until margin has been updated on browser element. (currently " + - containerWidth + "x" + containerHeight + ")"); - await promiseRounding; - - info(caseString + "Get innerWidth/Height from the content."); - await BrowserTestUtils.waitForCondition(async () => { - let {contentWidth, contentHeight} = await ContentTask.spawn( - tab.linkedBrowser, null, () => { - return { - contentWidth: content.innerWidth, - contentHeight: content.innerHeight, - }; - }); - - info(caseString + "Check the result."); - return aCheckFunc(contentWidth, contentHeight, containerWidth, containerHeight); - }, "Default Dimensions: The content window width is correctly rounded into."); - } - - BrowserTestUtils.removeTab(tab); -} - -async function test_customize_width_and_height(aWindow) { - const test_dimensions = `120x80, 200x143, 335x255, 600x312, 742x447, 813x558, - 990x672, 1200x733, 1470x858`; - - await SpecialPowers.pushPrefEnv({"set": - [ - ["privacy.resistFingerprinting.letterboxing.dimensions", test_dimensions], - ], - }); - - let dimensions_set = test_dimensions.split(",").map(item => { - let sizes = item.split("x").map(size => parseInt(size, 10)); - - return { - width: sizes[0], - height: sizes[1], - }; - }); - - let checkDimension = - (aContentWidth, aContentHeight, aRealWidth, aRealHeight) => { - let matchingArea = aRealWidth * aRealHeight; - let minWaste = Number.MAX_SAFE_INTEGER; - let targetDimensions = undefined; - - // Find the dimensions which waste the least content area. - for (let dim of dimensions_set) { - if (dim.width > aRealWidth || dim.height > aRealHeight) { - continue; - } - - let waste = matchingArea - dim.width * dim.height; - - if (waste >= 0 && waste < minWaste) { - targetDimensions = dim; - minWaste = waste; - } - } - - // This platform-specific code is explained in the large comment above. - if (getPlatform() != "linux") { - ok(handleOSFuzziness(aContentWidth, targetDimensions.width), - `Custom Dimension: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetDimensions.width}`); - - ok(handleOSFuzziness(aContentHeight, targetDimensions.height), - `Custom Dimension: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetDimensions.height}`); - - // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends - return true; - } - // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds. - return handleOSFuzziness(aContentWidth, targetDimensions.width) && handleOSFuzziness(aContentHeight, targetDimensions.height); - }; - - await test_dynamical_window_rounding(aWindow, checkDimension); - - await SpecialPowers.popPrefEnv(); -} - -async function test_no_rounding_for_chrome(aWindow) { - // First, resize the window to a size with is not rounded. - await new Promise(resolve => { - aWindow.onresize = () => resolve(); - aWindow.resizeTo(700, 450); - }); - - // open a chrome privilege tab, like about:config. - let tab = await BrowserTestUtils.openNewForegroundTab( - aWindow.gBrowser, "about:config"); - - // Check that the browser element should not have a margin. - is(tab.linkedBrowser.style.margin, "", "There is no margin around chrome tab."); - - BrowserTestUtils.removeTab(tab); -} - -add_task(async function setup() { - await SpecialPowers.pushPrefEnv({"set": - [ - ["privacy.resistFingerprinting.letterboxing", true], - ["privacy.resistFingerprinting.letterboxing.testing", true], - ], - }); -}); - -add_task(async function do_tests() { - // Store the original window size before testing. - let originalOuterWidth = window.outerWidth; - let originalOuterHeight = window.outerHeight; - - info("Run test for the default window rounding."); - await test_dynamical_window_rounding(window, checkForDefaultSetting); - - info("Run test for the window rounding with customized dimensions."); - await test_customize_width_and_height(window); - - info("Run test for no margin around tab with the chrome privilege."); - await test_no_rounding_for_chrome(window); - - // Restore the original window size. - window.outerWidth = originalOuterWidth; - window.outerHeight = originalOuterHeight; - - // Testing that whether the dynamical rounding works for new windows. - let win = await BrowserTestUtils.openNewBrowserWindow(); - - info("Run test for the default window rounding in new window."); - await test_dynamical_window_rounding(win, checkForDefaultSetting); - - info("Run test for the window rounding with customized dimensions in new window."); - await test_customize_width_and_height(win); - - info("Run test for no margin around tab with the chrome privilege in new window."); - await test_no_rounding_for_chrome(win); - - await BrowserTestUtils.closeWindow(win); -}); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 1ec585d0bd1c..471e844a1dbe 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1442,9 +1442,6 @@ pref("privacy.firstparty.isolate.restrict_opener_access", true); // If you do set it, to work around some broken website, please file a bug with // information so we can understand why it is needed. pref("privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", true); -// The log level for browser console messages logged in RFPHelper.jsm -// Change to 'All' and restart to see the messages -pref("privacy.resistFingerprinting.jsmloglevel", "Warn"); // A subset of Resist Fingerprinting protections focused specifically on timers for testing // This affects the Animation API, the performance APIs, Date.getTime, Event.timestamp, // File.lastModified, audioContext.currentTime, canvas.captureStream.currentTime diff --git a/toolkit/components/resistfingerprinting/LanguagePrompt.jsm b/toolkit/components/resistfingerprinting/LanguagePrompt.jsm new file mode 100644 index 000000000000..3f90244ccc4b --- /dev/null +++ b/toolkit/components/resistfingerprinting/LanguagePrompt.jsm @@ -0,0 +1,201 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["LanguagePrompt"]; + +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const kPrefResistFingerprinting = "privacy.resistFingerprinting"; +const kPrefSpoofEnglish = "privacy.spoof_english"; +const kTopicHttpOnModifyRequest = "http-on-modify-request"; + +class _LanguagePrompt { + constructor() { + this._initialized = false; + } + + init() { + if (this._initialized) { + return; + } + this._initialized = true; + + Services.prefs.addObserver(kPrefResistFingerprinting, this); + this._handleResistFingerprintingChanged(); + } + + uninit() { + if (!this._initialized) { + return; + } + this._initialized = false; + + Services.prefs.removeObserver(kPrefResistFingerprinting, this); + this._removeObservers(); + } + + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + this._handlePrefChanged(data); + break; + case kTopicHttpOnModifyRequest: + this._handleHttpOnModifyRequest(subject, data); + break; + default: + break; + } + } + + _removeObservers() { + try { + Services.pref.removeObserver(kPrefSpoofEnglish, this); + } catch (e) { + // do nothing + } + try { + Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); + } catch (e) { + // do nothing + } + } + + _shouldPromptForLanguagePref() { + return (Services.locale.appLocaleAsLangTag.substr(0, 2) !== "en") + && (Services.prefs.getIntPref(kPrefSpoofEnglish) === 0); + } + + _handlePrefChanged(data) { + switch (data) { + case kPrefResistFingerprinting: + this._handleResistFingerprintingChanged(); + break; + case kPrefSpoofEnglish: + this._handleSpoofEnglishChanged(); + break; + default: + break; + } + } + + _handleResistFingerprintingChanged() { + if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) { + Services.prefs.addObserver(kPrefSpoofEnglish, this); + if (this._shouldPromptForLanguagePref()) { + Services.obs.addObserver(this, kTopicHttpOnModifyRequest); + } + } else { + this._removeObservers(); + } + } + + _handleSpoofEnglishChanged() { + switch (Services.prefs.getIntPref(kPrefSpoofEnglish)) { + case 0: // will prompt + // This should only happen when turning privacy.resistFingerprinting off. + // Works like disabling accept-language spoofing. + case 1: // don't spoof + if (Services.prefs.prefHasUserValue("javascript.use_us_english_locale")) { + Services.prefs.clearUserPref("javascript.use_us_english_locale"); + } + // We don't reset intl.accept_languages. Instead, setting + // privacy.spoof_english to 1 allows user to change preferred language + // settings through Preferences UI. + break; + case 2: // spoof + Services.prefs.setCharPref("intl.accept_languages", "en-US, en"); + Services.prefs.setBoolPref("javascript.use_us_english_locale", true); + break; + default: + break; + } + } + + _handleHttpOnModifyRequest(subject, data) { + // If we are loading an HTTP page from content, show the + // "request English language web pages?" prompt. + let httpChannel; + try { + httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + } catch (e) { + return; + } + + if (!httpChannel) { + return; + } + + let notificationCallbacks = httpChannel.notificationCallbacks; + if (!notificationCallbacks) { + return; + } + + let loadContext = notificationCallbacks.getInterface(Ci.nsILoadContext); + if (!loadContext || !loadContext.isContent) { + return; + } + + if (!subject.URI.schemeIs("http") && !subject.URI.schemeIs("https")) { + return; + } + // The above QI did not throw, the scheme is http[s], and we know the + // load context is content, so we must have a true HTTP request from content. + // Stop the observer and display the prompt if another window has + // not already done so. + Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); + + if (!this._shouldPromptForLanguagePref()) { + return; + } + + this._promptForLanguagePreference(); + + // The Accept-Language header for this request was set when the + // channel was created. Reset it to match the value that will be + // used for future requests. + let val = this._getCurrentAcceptLanguageValue(subject.URI); + if (val) { + httpChannel.setRequestHeader("Accept-Language", val, false); + } + } + + _promptForLanguagePreference() { + // Display two buttons, both with string titles. + let flags = Services.prompt.STD_YES_NO_BUTTONS; + let brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties"); + let brandShortName = brandBundle.GetStringFromName("brandShortName"); + let navigatorBundle = Services.strings.createBundle( + "chrome://browser/locale/browser.properties"); + let message = navigatorBundle.formatStringFromName( + "privacy.spoof_english", [brandShortName], 1); + let response = Services.prompt.confirmEx( + null, "", message, flags, null, null, null, null, {value: false}); + + // Update preferences to reflect their response and to prevent the prompt + // from being displayed again. + Services.prefs.setIntPref(kPrefSpoofEnglish, (response == 0) ? 2 : 1); + } + + _getCurrentAcceptLanguageValue(uri) { + let channel = Services.io.newChannelFromURI2( + uri, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + let httpChannel; + try { + httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + } catch (e) { + return null; + } + return httpChannel.getRequestHeader("Accept-Language"); + } +} + +let LanguagePrompt = new _LanguagePrompt(); diff --git a/toolkit/components/resistfingerprinting/RFPHelper.jsm b/toolkit/components/resistfingerprinting/RFPHelper.jsm deleted file mode 100644 index 010443f6b33b..000000000000 --- a/toolkit/components/resistfingerprinting/RFPHelper.jsm +++ /dev/null @@ -1,531 +0,0 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -var EXPORTED_SYMBOLS = ["RFPHelper"]; - -const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); - -const kPrefResistFingerprinting = "privacy.resistFingerprinting"; -const kPrefSpoofEnglish = "privacy.spoof_english"; -const kTopicHttpOnModifyRequest = "http-on-modify-request"; - -const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing"; -const kPrefLetterboxingDimensions = - "privacy.resistFingerprinting.letterboxing.dimensions"; -const kPrefLetterboxingTesting = - "privacy.resistFingerprinting.letterboxing.testing"; -const kTopicDOMWindowOpened = "domwindowopened"; -const kEventLetterboxingSizeUpdate = "Letterboxing:ContentSizeUpdated"; - -const kDefaultWidthStepping = 200; -const kDefaultHeightStepping = 100; - -var logConsole; -function log(msg) { - if (!logConsole) { - logConsole = console.createInstance({ - prefix: "RFPHelper.jsm", - maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel", - }); - } - - logConsole.log(msg); -} - -class _RFPHelper { - // ============================================================================ - // Shared Setup - // ============================================================================ - constructor() { - this._initialized = false; - } - - init() { - if (this._initialized) { - return; - } - this._initialized = true; - - // Add unconditional observers - Services.prefs.addObserver(kPrefResistFingerprinting, this); - Services.prefs.addObserver(kPrefLetterboxing, this); - XPCOMUtils.defineLazyPreferenceGetter(this, "_letterboxingDimensions", - kPrefLetterboxingDimensions, "", null, this._parseLetterboxingDimensions); - XPCOMUtils.defineLazyPreferenceGetter(this, "_isLetterboxingTesting", - kPrefLetterboxingTesting, false); - - // Add RFP and Letterboxing observers if prefs are enabled - this._handleResistFingerprintingChanged(); - this._handleLetterboxingPrefChanged(); - } - - uninit() { - if (!this._initialized) { - return; - } - this._initialized = false; - - // Remove unconditional observers - Services.prefs.removeObserver(kPrefResistFingerprinting, this); - Services.prefs.removeObserver(kPrefLetterboxing, this); - // Remove the RFP observers, swallowing exceptions if they weren't present - this._removeRFPObservers(); - } - - observe(subject, topic, data) { - switch (topic) { - case "nsPref:changed": - this._handlePrefChanged(data); - break; - case kTopicHttpOnModifyRequest: - this._handleHttpOnModifyRequest(subject, data); - break; - case kTopicDOMWindowOpened: - // We attach to the newly created window by adding tabsProgressListener - // and event listener on it. We listen for new tabs being added or - // the change of the content principal and apply margins accordingly. - this._handleDOMWindowOpened(subject); - break; - default: - break; - } - } - - handleEvent(aMessage) { - switch (aMessage.type) { - case "TabOpen": - { - let tab = aMessage.target; - this._addOrClearContentMargin(tab.linkedBrowser); - break; - } - default: - break; - } - } - - receiveMessage(aMessage) { - switch (aMessage.name) { - case kEventLetterboxingSizeUpdate: - let win = aMessage.target.ownerGlobal; - this._updateMarginsForTabsInWindow(win); - break; - default: - break; - } - } - - _handlePrefChanged(data) { - switch (data) { - case kPrefResistFingerprinting: - this._handleResistFingerprintingChanged(); - break; - case kPrefSpoofEnglish: - this._handleSpoofEnglishChanged(); - break; - case kPrefLetterboxing: - this._handleLetterboxingPrefChanged(); - break; - default: - break; - } - } - - // ============================================================================ - // Language Prompt - // ============================================================================ - _addRFPObservers() { - Services.prefs.addObserver(kPrefSpoofEnglish, this); - if (this._shouldPromptForLanguagePref()) { - Services.obs.addObserver(this, kTopicHttpOnModifyRequest); - } - } - - _removeRFPObservers() { - try { - Services.pref.removeObserver(kPrefSpoofEnglish, this); - } catch (e) { - // do nothing - } - try { - Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); - } catch (e) { - // do nothing - } - } - - _handleResistFingerprintingChanged() { - if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) { - this._addRFPObservers(); - } else { - this._removeRFPObservers(); - } - } - - _handleSpoofEnglishChanged() { - switch (Services.prefs.getIntPref(kPrefSpoofEnglish)) { - case 0: // will prompt - // This should only happen when turning privacy.resistFingerprinting off. - // Works like disabling accept-language spoofing. - case 1: // don't spoof - if (Services.prefs.prefHasUserValue("javascript.use_us_english_locale")) { - Services.prefs.clearUserPref("javascript.use_us_english_locale"); - } - // We don't reset intl.accept_languages. Instead, setting - // privacy.spoof_english to 1 allows user to change preferred language - // settings through Preferences UI. - break; - case 2: // spoof - Services.prefs.setCharPref("intl.accept_languages", "en-US, en"); - Services.prefs.setBoolPref("javascript.use_us_english_locale", true); - break; - default: - break; - } - } - - _shouldPromptForLanguagePref() { - return (Services.locale.appLocaleAsLangTag.substr(0, 2) !== "en") - && (Services.prefs.getIntPref(kPrefSpoofEnglish) === 0); - } - - _handleHttpOnModifyRequest(subject, data) { - // If we are loading an HTTP page from content, show the - // "request English language web pages?" prompt. - let httpChannel; - try { - httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); - } catch (e) { - return; - } - - if (!httpChannel) { - return; - } - - let notificationCallbacks = httpChannel.notificationCallbacks; - if (!notificationCallbacks) { - return; - } - - let loadContext = notificationCallbacks.getInterface(Ci.nsILoadContext); - if (!loadContext || !loadContext.isContent) { - return; - } - - if (!subject.URI.schemeIs("http") && !subject.URI.schemeIs("https")) { - return; - } - // The above QI did not throw, the scheme is http[s], and we know the - // load context is content, so we must have a true HTTP request from content. - // Stop the observer and display the prompt if another window has - // not already done so. - Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); - - if (!this._shouldPromptForLanguagePref()) { - return; - } - - this._promptForLanguagePreference(); - - // The Accept-Language header for this request was set when the - // channel was created. Reset it to match the value that will be - // used for future requests. - let val = this._getCurrentAcceptLanguageValue(subject.URI); - if (val) { - httpChannel.setRequestHeader("Accept-Language", val, false); - } - } - - _promptForLanguagePreference() { - // Display two buttons, both with string titles. - let flags = Services.prompt.STD_YES_NO_BUTTONS; - let brandBundle = Services.strings.createBundle( - "chrome://branding/locale/brand.properties"); - let brandShortName = brandBundle.GetStringFromName("brandShortName"); - let navigatorBundle = Services.strings.createBundle( - "chrome://browser/locale/browser.properties"); - let message = navigatorBundle.formatStringFromName( - "privacy.spoof_english", [brandShortName], 1); - let response = Services.prompt.confirmEx( - null, "", message, flags, null, null, null, null, {value: false}); - - // Update preferences to reflect their response and to prevent the prompt - // from being displayed again. - Services.prefs.setIntPref(kPrefSpoofEnglish, (response == 0) ? 2 : 1); - } - - _getCurrentAcceptLanguageValue(uri) { - let channel = Services.io.newChannelFromURI2( - uri, - null, // aLoadingNode - Services.scriptSecurityManager.getSystemPrincipal(), - null, // aTriggeringPrincipal - Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, - Ci.nsIContentPolicy.TYPE_OTHER); - let httpChannel; - try { - httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); - } catch (e) { - return null; - } - return httpChannel.getRequestHeader("Accept-Language"); - } - - // ============================================================================== - // Letterboxing - // ============================================================================ - /** - * We use the TabsProgressListener to catch the change of the content - * principal. We would clear the margins around the content viewport if - * it is the system principal. - */ - onLocationChange(aBrowser) { - this._addOrClearContentMargin(aBrowser); - } - - _handleLetterboxingPrefChanged() { - if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) { - Services.ww.registerNotification(this); - this._attachAllWindows(); - } else { - this._detachAllWindows(); - Services.ww.unregisterNotification(this); - } - } - - // The function to parse the dimension set from the pref value. The pref value - // should be formated as 'width1xheight1, width2xheight2, ...'. For - // example, '100x100, 200x200, 400x200 ...'. - _parseLetterboxingDimensions(aPrefValue) { - if (!aPrefValue || !aPrefValue.match(/^(?:\d+x\d+,\s*)*(?:\d+x\d+)$/)) { - if (aPrefValue) { - Cu.reportError(`Invalid pref value for ${kPrefLetterboxingDimensions}: ${aPrefValue}`); - } - return []; - } - - return aPrefValue.split(",").map(item => { - let sizes = item.split("x").map(size => parseInt(size, 10)); - - return { - width: sizes[0], - height: sizes[1], - }; - }); - } - - _addOrClearContentMargin(aBrowser) { - let tab = aBrowser.getTabBrowser() - .getTabForBrowser(aBrowser); - - // We won't do anything for lazy browsers. - if (!aBrowser.isConnected) { - return; - } - - // We should apply no margin around an empty tab or a tab with system - // principal. - if (tab.isEmpty || aBrowser.contentPrincipal.isSystemPrincipal) { - this._clearContentViewMargin(aBrowser); - } else { - this._roundContentView(aBrowser); - } - } - - /** - * The function will round the given browser by adding margins around the - * content viewport. - */ - async _roundContentView(aBrowser) { - let logId = Math.random(); - log("_roundContentView[" + logId + "]"); - let win = aBrowser.ownerGlobal; - let browserContainer = aBrowser.getTabBrowser() - .getBrowserContainer(aBrowser); - - let {contentWidth, contentHeight, containerWidth, containerHeight} = - await win.promiseDocumentFlushed(() => { - let contentWidth = aBrowser.clientWidth; - let contentHeight = aBrowser.clientHeight; - let containerWidth = browserContainer.clientWidth; - let containerHeight = browserContainer.clientHeight; - - return { - contentWidth, - contentHeight, - containerWidth, - containerHeight, - }; - }); - - log("_roundContentView[" + logId + "] contentWidth=" + contentWidth + " contentHeight=" + contentHeight + - " containerWidth=" + containerWidth + " containerHeight=" + containerHeight + " "); - - let calcMargins = (aWidth, aHeight) => { - let result; - log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ")"); - // If the set is empty, we will round the content with the default - // stepping size. - if (!this._letterboxingDimensions.length) { - result = { - width: (aWidth % kDefaultWidthStepping) / 2, - height: (aHeight % kDefaultHeightStepping) / 2, - }; - log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ") = " + result.width + " x " + result.height); - return result; - } - - let matchingArea = aWidth * aHeight; - let minWaste = Number.MAX_SAFE_INTEGER; - let targetDimensions = undefined; - - // Find the desired dimensions which waste the least content area. - for (let dim of this._letterboxingDimensions) { - // We don't need to consider the dimensions which cannot fit into the - // real content size. - if (dim.width > aWidth || dim.height > aHeight) { - continue; - } - - let waste = matchingArea - dim.width * dim.height; - - if (waste >= 0 && waste < minWaste) { - targetDimensions = dim; - minWaste = waste; - } - } - - // If we cannot find any dimensions match to the real content window, this - // means the content area is smaller the smallest size in the set. In this - // case, we won't apply any margins. - if (!targetDimensions) { - result = { - width: 0, - height: 0, - }; - } else { - result = { - width: (aWidth - targetDimensions.width) / 2, - height: (aHeight - targetDimensions.height) / 2, - }; - } - - log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ") = " + result.width + " x " + result.height); - return result; - }; - - // Calculating the margins around the browser element in order to round the - // content viewport. We will use a 200x100 stepping if the dimension set - // is not given. - let margins = calcMargins(containerWidth, containerHeight); - - // If the size of the content is already quantized, we do nothing. - if (aBrowser.style.margin == `${margins.height}px ${margins.width}px`) { - log("_roundContentView[" + logId + "] is_rounded == true"); - if (this._isLetterboxingTesting) { - log("_roundContentView[" + logId + "] is_rounded == true test:letterboxing:update-margin-finish"); - Services.obs.notifyObservers(null, "test:letterboxing:update-margin-finish"); - } - return; - } - - win.requestAnimationFrame(() => { - log("_roundContentView[" + logId + "] setting margins to " + margins.width + " x " + margins.height); - // One cannot (easily) control the color of a margin unfortunately. - // An initial attempt to use a border instead of a margin resulted - // in offset event dispatching; so for now we use a colorless margin. - aBrowser.style.margin = `${margins.height}px ${margins.width}px`; - }); - } - - _clearContentViewMargin(aBrowser) { - aBrowser.ownerGlobal.requestAnimationFrame(() => { - aBrowser.style.margin = ""; - }); - } - - _updateMarginsForTabsInWindow(aWindow) { - let tabBrowser = aWindow.gBrowser; - - for (let tab of tabBrowser.tabs) { - let browser = tab.linkedBrowser; - this._addOrClearContentMargin(browser); - } - } - - _attachWindow(aWindow) { - aWindow.gBrowser - .addTabsProgressListener(this); - aWindow.addEventListener("TabOpen", this); - aWindow.messageManager - .addMessageListener(kEventLetterboxingSizeUpdate, this); - - // Rounding the content viewport. - this._updateMarginsForTabsInWindow(aWindow); - } - - _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 tabBrowser = aWindow.gBrowser; - tabBrowser.removeTabsProgressListener(this); - aWindow.removeEventListener("TabOpen", this); - aWindow.messageManager - .removeMessageListener(kEventLetterboxingSizeUpdate, this); - - // Clear all margins and tooltip for all browsers. - for (let tab of tabBrowser.tabs) { - let browser = tab.linkedBrowser; - this._clearContentViewMargin(browser); - } - } - - _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(aSubject) { - let win = aSubject.QueryInterface(Ci.nsIDOMWindow); - let self = this; - - win.addEventListener("load", () => { - // We attach to the new window when it has been loaded if the new loaded - // window is a browsing window. - if (win.document - .documentElement - .getAttribute("windowtype") !== "navigator:browser") { - return; - } - self._attachWindow(win); - }, {once: true}); - } -} - -let RFPHelper = new _RFPHelper(); diff --git a/toolkit/components/resistfingerprinting/moz.build b/toolkit/components/resistfingerprinting/moz.build index 021f2786f966..88525f6189eb 100644 --- a/toolkit/components/resistfingerprinting/moz.build +++ b/toolkit/components/resistfingerprinting/moz.build @@ -21,5 +21,5 @@ EXPORTS.mozilla += [ ] EXTRA_JS_MODULES += [ - 'RFPHelper.jsm', + 'LanguagePrompt.jsm', ]