Bug 1039069 - Provide a popup about English for international users. r=arthuredelstein,mconley draft
authorChung-Sheng Fu <cfu@mozilla.com>
Tue, 12 Sep 2017 17:32:07 +0800
changeset 706239 6c55e1fca3448653895c9f78d7b412dd4ca785c4
parent 706205 781485c695e1f07b8782427d556f6570e4a8072f
child 706240 e8b5a7e79312f73809c22f21c06eedacf167f7d1
push id91750
push userbmo:cfu@mozilla.com
push dateFri, 01 Dec 2017 15:12:32 +0000
reviewersarthuredelstein, mconley
bugs1039069
milestone59.0a1
Bug 1039069 - Provide a popup about English for international users. r=arthuredelstein,mconley MozReview-Commit-ID: IL8i4vzjWQd
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
toolkit/components/resistfingerprinting/LanguagePrompt.jsm
toolkit/components/resistfingerprinting/moz.build
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -577,16 +577,22 @@ pref("privacy.sanitize.sanitizeOnShutdow
 
 pref("privacy.sanitize.migrateFx3Prefs",    false);
 
 pref("privacy.panicButton.enabled",         true);
 
 // Time until temporary permissions expire, in ms
 pref("privacy.temporary_permission_expire_time_ms",  3600000);
 
+// If Accept-Language should be spoofed by en-US
+// 0 - will prompt
+// 1 - don't spoof
+// 2 - spoof
+pref("privacy.spoof_english", 0);
+
 pref("network.proxy.share_proxy_settings",  false); // use the same proxy settings for all protocols
 
 // simple gestures support
 pref("browser.gesture.swipe.left", "Browser:BackOrBackDuplicate");
 pref("browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate");
 pref("browser.gesture.swipe.up", "cmd_scrollTop");
 pref("browser.gesture.swipe.down", "cmd_scrollBottom");
 #ifdef XP_MACOSX
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ContentSearch: "resource:///modules/ContentSearch.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   Deprecated: "resource://gre/modules/Deprecated.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
   E10SUtils: "resource:///modules/E10SUtils.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
+  LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   Log: "resource://gre/modules/Log.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
@@ -1816,16 +1817,18 @@ var gBrowserInit = {
     CaptivePortalWatcher.uninit();
 
     SidebarUI.uninit();
 
     DownloadsButton.uninit();
 
     gAccessibilityServiceIndicator.uninit();
 
+    LanguagePrompt.uninit();
+
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
     if (this._boundDelayedStartup) {
       this._cancelDelayedStartup();
     } else {
       if (Win7Features)
         Win7Features.onCloseWindow();
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   Integration: "resource://gre/modules/Integration.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
+  LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   LoginHelper: "resource://gre/modules/LoginHelper.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
@@ -1161,16 +1162,20 @@ BrowserGlue.prototype = {
       handlerService.asyncInit();
     });
 
     if (AppConstants.platform == "win") {
       Services.tm.idleDispatchToMainThread(() => {
         JawsScreenReaderVersionCheck.onWindowsRestored();
       });
     }
+
+    Services.tm.idleDispatchToMainThread(() => {
+      LanguagePrompt.init();
+    });
   },
 
   /**
    * Use this function as an entry point to schedule tasks that need
    * to run once per session, at any arbitrary point in time.
    * This function will be called from an idle observer. Check the value of
    * LATE_TASKS_IDLE_TIME_SEC to see the current value for this idle
    * observer.
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -492,16 +492,19 @@ offlineApps.manageUsageAccessKey=S
 # LOCALIZATION NOTE (canvas.siteprompt): %S is hostname
 canvas.siteprompt=Will you allow %S to use your HTML5 canvas image data? This may be used to uniquely identify your computer.
 canvas.notAllow=Don’t Allow
 canvas.notAllow.accesskey=n
 canvas.allow=Allow Data Access
 canvas.allow.accesskey=A
 canvas.remember=Always remember my decision
 
+# Spoof Accept-Language prompt
+privacy.spoof_english=Changing your language setting to English will make you more difficult to identify and enhance your privacy. Do you want to request English language versions of web pages?
+
 identity.identified.verifier=Verified by: %S
 identity.identified.verified_by_you=You have added a security exception for this site.
 identity.identified.state_and_country=%S, %S
 
 identity.icon.tooltip=Show site information
 identity.extension.label=Extension (%S)
 identity.extension.tooltip=Loaded by extension: %S
 identity.showDetails.tooltip=Show connection details
new file mode 100644
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/LanguagePrompt.jsm
@@ -0,0 +1,205 @@
+// -*- 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";
+
+this.EXPORTED_SYMBOLS = ["LanguagePrompt"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.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.getAppLocaleAsLangTag().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();
+      Services.prefs.setIntPref(kPrefSpoofEnglish, 0);
+    }
+  }
+
+  _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();
--- a/toolkit/components/resistfingerprinting/moz.build
+++ b/toolkit/components/resistfingerprinting/moz.build
@@ -8,8 +8,12 @@ UNIFIED_SOURCES += [
     'nsRFPService.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 EXPORTS += [
     'nsRFPService.h',
 ]
+
+EXTRA_JS_MODULES += [
+    'LanguagePrompt.jsm',
+]