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/. */
5"use strict";
6
7var EXPORTED_SYMBOLS = ["LanguagePrompt"];
8
9ChromeUtils.import("resource://gre/modules/Services.jsm");
10ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
11
12const kPrefResistFingerprinting = "privacy.resistFingerprinting";
13const kPrefSpoofEnglish = "privacy.spoof_english";
14const kTopicHttpOnModifyRequest = "http-on-modify-request";
15
16class _LanguagePrompt {
17  constructor() {
18    this._initialized = false;
19  }
20
21  init() {
22    if (this._initialized) {
23      return;
24    }
25    this._initialized = true;
26
27    Services.prefs.addObserver(kPrefResistFingerprinting, this);
28    this._handleResistFingerprintingChanged();
29  }
30
31  uninit() {
32    if (!this._initialized) {
33      return;
34    }
35    this._initialized = false;
36
37    Services.prefs.removeObserver(kPrefResistFingerprinting, this);
38    this._removeObservers();
39  }
40
41  observe(subject, topic, data) {
42    switch (topic) {
43      case "nsPref:changed":
44        this._handlePrefChanged(data);
45        break;
46      case kTopicHttpOnModifyRequest:
47        this._handleHttpOnModifyRequest(subject, data);
48        break;
49      default:
50        break;
51    }
52  }
53
54  _removeObservers() {
55    try {
56      Services.pref.removeObserver(kPrefSpoofEnglish, this);
57    } catch (e) {
58      // do nothing
59    }
60    try {
61      Services.obs.removeObserver(this, kTopicHttpOnModifyRequest);
62    } catch (e) {
63      // do nothing
64    }
65  }
66
67  _shouldPromptForLanguagePref() {
68    return (Services.locale.getAppLocaleAsLangTag().substr(0, 2) !== "en")
69      && (Services.prefs.getIntPref(kPrefSpoofEnglish) === 0);
70  }
71
72  _handlePrefChanged(data) {
73    switch (data) {
74      case kPrefResistFingerprinting:
75        this._handleResistFingerprintingChanged();
76        break;
77      case kPrefSpoofEnglish:
78        this._handleSpoofEnglishChanged();
79        break;
80      default:
81        break;
82    }
83  }
84
85  _handleResistFingerprintingChanged() {
86    if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) {
87      Services.prefs.addObserver(kPrefSpoofEnglish, this);
88      if (this._shouldPromptForLanguagePref()) {
89        Services.obs.addObserver(this, kTopicHttpOnModifyRequest);
90      }
91    } else {
92      this._removeObservers();
93      Services.prefs.setIntPref(kPrefSpoofEnglish, 0);
94    }
95  }
96
97  _handleSpoofEnglishChanged() {
98    switch (Services.prefs.getIntPref(kPrefSpoofEnglish)) {
99      case 0: // will prompt
100        // This should only happen when turning privacy.resistFingerprinting off.
101        // Works like disabling accept-language spoofing.
102      case 1: // don't spoof
103        if (Services.prefs.prefHasUserValue("javascript.use_us_english_locale")) {
104          Services.prefs.clearUserPref("javascript.use_us_english_locale");
105        }
106        // We don't reset intl.accept_languages. Instead, setting
107        // privacy.spoof_english to 1 allows user to change preferred language
108        // settings through Preferences UI.
109        break;
110      case 2: // spoof
111        Services.prefs.setCharPref("intl.accept_languages", "en-US, en");
112        Services.prefs.setBoolPref("javascript.use_us_english_locale", true);
113        break;
114      default:
115        break;
116    }
117  }
118
119  _handleHttpOnModifyRequest(subject, data) {
120    // If we are loading an HTTP page from content, show the
121    // "request English language web pages?" prompt.
122    let httpChannel;
123    try {
124      httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
125    } catch (e) {
126      return;
127    }
128
129    if (!httpChannel) {
130      return;
131    }
132
133    let notificationCallbacks = httpChannel.notificationCallbacks;
134    if (!notificationCallbacks) {
135      return;
136    }
137
138    let loadContext = notificationCallbacks.getInterface(Ci.nsILoadContext);
139    if (!loadContext || !loadContext.isContent) {
140      return;
141    }
142
143    if (!subject.URI.schemeIs("http") && !subject.URI.schemeIs("https")) {
144      return;
145    }
146    // The above QI did not throw, the scheme is http[s], and we know the
147    // load context is content, so we must have a true HTTP request from content.
148    // Stop the observer and display the prompt if another window has
149    // not already done so.
150    Services.obs.removeObserver(this, kTopicHttpOnModifyRequest);
151
152    if (!this._shouldPromptForLanguagePref()) {
153      return;
154    }
155
156    this._promptForLanguagePreference();
157
158    // The Accept-Language header for this request was set when the
159    // channel was created. Reset it to match the value that will be
160    // used for future requests.
161    let val = this._getCurrentAcceptLanguageValue(subject.URI);
162    if (val) {
163      httpChannel.setRequestHeader("Accept-Language", val, false);
164    }
165  }
166
167  _promptForLanguagePreference() {
168    // Display two buttons, both with string titles.
169    let flags = Services.prompt.STD_YES_NO_BUTTONS;
170    let brandBundle = Services.strings.createBundle(
171      "chrome://branding/locale/brand.properties");
172    let brandShortName = brandBundle.GetStringFromName("brandShortName");
173    let navigatorBundle = Services.strings.createBundle(
174      "chrome://browser/locale/browser.properties");
175    let message = navigatorBundle.formatStringFromName(
176      "privacy.spoof_english", [brandShortName], 1);
177    let response = Services.prompt.confirmEx(
178      null, "", message, flags, null, null, null, null, {value: false});
179
180    // Update preferences to reflect their response and to prevent the prompt
181    // from being displayed again.
182    Services.prefs.setIntPref(kPrefSpoofEnglish, (response == 0) ? 2 : 1);
183  }
184
185  _getCurrentAcceptLanguageValue(uri) {
186    let channel = Services.io.newChannelFromURI2(
187        uri,
188        null, // aLoadingNode
189        Services.scriptSecurityManager.getSystemPrincipal(),
190        null, // aTriggeringPrincipal
191        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
192        Ci.nsIContentPolicy.TYPE_OTHER);
193    let httpChannel;
194    try {
195      httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
196    } catch (e) {
197      return null;
198    }
199    return httpChannel.getRequestHeader("Accept-Language");
200  }
201}
202
203let LanguagePrompt = new _LanguagePrompt();
204