tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

languages.js (13759B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
      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
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 /* import-globals-from /toolkit/content/preferencesBindings.js */
      7 
      8 Preferences.addAll([
      9  { id: "intl.accept_languages", type: "string" },
     10  { id: "pref.browser.language.disable_button.up", type: "bool" },
     11  { id: "pref.browser.language.disable_button.down", type: "bool" },
     12  { id: "pref.browser.language.disable_button.remove", type: "bool" },
     13  { id: "privacy.spoof_english", type: "int" },
     14 ]);
     15 Preferences.addSetting({
     16  id: "acceptLanguages",
     17  pref: "intl.accept_languages",
     18  get(prefVal, _, setting) {
     19    return setting.pref.defaultValue != prefVal
     20      ? prefVal
     21      : Services.locale.acceptLanguages;
     22  },
     23 });
     24 
     25 var gLanguagesDialog = {
     26  _availableLanguagesList: [],
     27  _acceptLanguages: {},
     28 
     29  _selectedItemID: null,
     30 
     31  onLoad() {
     32    let spoofEnglishElement = document.getElementById("spoofEnglish");
     33    Preferences.addSyncFromPrefListener(spoofEnglishElement, () =>
     34      gLanguagesDialog.readSpoofEnglish()
     35    );
     36    Preferences.addSyncToPrefListener(spoofEnglishElement, () =>
     37      gLanguagesDialog.writeSpoofEnglish()
     38    );
     39 
     40    Preferences.getSetting("acceptLanguages").on("change", () =>
     41      this._readAcceptLanguages().catch(console.error)
     42    );
     43 
     44    let addListener = (id, cmd) => {
     45      document.getElementById(id).addEventListener(cmd, this);
     46    };
     47    addListener("LanguagesDialog", "command");
     48    addListener("availableLanguages", "command");
     49    addListener("activeLanguages", "select");
     50 
     51    if (!this._availableLanguagesList.length) {
     52      document.mozSubdialogReady = this._loadAvailableLanguages();
     53    }
     54  },
     55 
     56  get _activeLanguages() {
     57    return document.getElementById("activeLanguages");
     58  },
     59 
     60  get _availableLanguages() {
     61    return document.getElementById("availableLanguages");
     62  },
     63 
     64  async _loadAvailableLanguages() {
     65    // This is a parser for: resource://gre/res/language.properties
     66    // The file is formatted like so:
     67    // ab[-cd].accept=true|false
     68    //  ab = language
     69    //  cd = region
     70    var bundleAccepted = document.getElementById("bundleAccepted");
     71 
     72    function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) {
     73      this.name = aLocaleName;
     74      this.code = aLocaleCode;
     75      this.isVisible = aIsVisible;
     76    }
     77 
     78    // 1) Read the available languages out of language.properties
     79 
     80    let localeCodes = [];
     81    let localeValues = [];
     82    for (let currString of bundleAccepted.strings) {
     83      var property = currString.key.split("."); // ab[-cd].accept
     84      if (property[1] == "accept") {
     85        localeCodes.push(property[0]);
     86        localeValues.push(currString.value);
     87      }
     88    }
     89 
     90    let localeNames = Services.intl.getLocaleDisplayNames(
     91      undefined,
     92      localeCodes
     93    );
     94 
     95    for (let i in localeCodes) {
     96      let isVisible =
     97        localeValues[i] == "true" &&
     98        (!(localeCodes[i] in this._acceptLanguages) ||
     99          !this._acceptLanguages[localeCodes[i]]);
    100 
    101      let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible);
    102      this._availableLanguagesList.push(li);
    103    }
    104 
    105    await this._buildAvailableLanguageList();
    106    await this._readAcceptLanguages();
    107  },
    108 
    109  async _buildAvailableLanguageList() {
    110    var availableLanguagesPopup = document.getElementById(
    111      "availableLanguagesPopup"
    112    );
    113    while (availableLanguagesPopup.hasChildNodes()) {
    114      availableLanguagesPopup.firstChild.remove();
    115    }
    116 
    117    let frag = document.createDocumentFragment();
    118 
    119    // Load the UI with the data
    120    for (var i = 0; i < this._availableLanguagesList.length; ++i) {
    121      let locale = this._availableLanguagesList[i];
    122      let localeCode = locale.code;
    123      if (
    124        locale.isVisible &&
    125        (!(localeCode in this._acceptLanguages) ||
    126          !this._acceptLanguages[localeCode])
    127      ) {
    128        var menuitem = document.createXULElement("menuitem");
    129        menuitem.id = localeCode;
    130        document.l10n.setAttributes(menuitem, "languages-code-format", {
    131          locale: locale.name,
    132          code: localeCode,
    133        });
    134        frag.appendChild(menuitem);
    135      }
    136    }
    137 
    138    await document.l10n.translateFragment(frag);
    139 
    140    // Sort the list of languages by name
    141    let comp = new Services.intl.Collator(undefined, {
    142      usage: "sort",
    143    });
    144 
    145    let items = Array.from(frag.children);
    146 
    147    items.sort((a, b) => {
    148      return comp.compare(a.getAttribute("label"), b.getAttribute("label"));
    149    });
    150 
    151    // Re-append items in the correct order:
    152    items.forEach(item => frag.appendChild(item));
    153 
    154    availableLanguagesPopup.appendChild(frag);
    155 
    156    this._availableLanguages.setAttribute(
    157      "label",
    158      this._availableLanguages.getAttribute("placeholder")
    159    );
    160  },
    161 
    162  async _readAcceptLanguages() {
    163    while (this._activeLanguages.hasChildNodes()) {
    164      this._activeLanguages.firstChild.remove();
    165    }
    166 
    167    var selectedIndex = 0;
    168    var preference = Preferences.getSetting("acceptLanguages");
    169    if (preference.value == "") {
    170      this._activeLanguages.selectedIndex = -1;
    171      this.onLanguageSelect();
    172      return;
    173    }
    174    var languages = preference.value.toLowerCase().split(/\s*,\s*/);
    175    for (var i = 0; i < languages.length; ++i) {
    176      var listitem = document.createXULElement("richlistitem");
    177      var label = document.createXULElement("label");
    178      listitem.appendChild(label);
    179      listitem.id = languages[i];
    180      if (languages[i] == this._selectedItemID) {
    181        selectedIndex = i;
    182      }
    183      this._activeLanguages.appendChild(listitem);
    184      var localeName = this._getLocaleName(languages[i]);
    185      document.l10n.setAttributes(label, "languages-active-code-format", {
    186        locale: localeName,
    187        code: languages[i],
    188      });
    189 
    190      // Hash this language as an "Active" language so we don't
    191      // show it in the list that can be added.
    192      this._acceptLanguages[languages[i]] = true;
    193    }
    194 
    195    // We're forcing an early localization here because otherwise
    196    // the initial sizing of the dialog will happen before it and
    197    // result in overflow.
    198    await document.l10n.translateFragment(this._activeLanguages);
    199 
    200    if (this._activeLanguages.childNodes.length) {
    201      this._activeLanguages.ensureIndexIsVisible(selectedIndex);
    202      this._activeLanguages.selectedIndex = selectedIndex;
    203    }
    204 
    205    // Update states of accept-language list and buttons according to
    206    // privacy.resistFingerprinting and privacy.spoof_english.
    207    this.readSpoofEnglish();
    208  },
    209 
    210  handleEvent(event) {
    211    switch (event.type) {
    212      case "command":
    213        if (event.currentTarget.id == "availableLanguages") {
    214          this.onAvailableLanguageSelect();
    215          break;
    216        }
    217 
    218        switch (event.target.id) {
    219          case "key_close":
    220            Preferences.close(event);
    221            break;
    222 
    223          case "up":
    224            this.moveUp();
    225            break;
    226          case "down":
    227            this.moveDown();
    228            break;
    229          case "remove":
    230            this.removeLanguage();
    231            break;
    232          case "addButton":
    233            this.addLanguage();
    234            break;
    235        }
    236        break;
    237      case "dialoghelp":
    238        window.top.openPrefsHelp();
    239        break;
    240      case "load":
    241        this.onLoad();
    242        break;
    243      case "select":
    244        this.onLanguageSelect();
    245        break;
    246    }
    247  },
    248 
    249  onAvailableLanguageSelect() {
    250    var availableLanguages = this._availableLanguages;
    251    var addButton = document.getElementById("addButton");
    252    addButton.disabled =
    253      availableLanguages.disabled || availableLanguages.selectedIndex < 0;
    254 
    255    this._availableLanguages.removeAttribute("accesskey");
    256  },
    257 
    258  addLanguage() {
    259    var selectedID = this._availableLanguages.selectedItem.id;
    260    var preference = Preferences.getSetting("acceptLanguages");
    261    var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/);
    262    for (var i = 0; i < arrayOfPrefs.length; ++i) {
    263      if (arrayOfPrefs[i] == selectedID) {
    264        return;
    265      }
    266    }
    267 
    268    this._selectedItemID = selectedID;
    269 
    270    if (preference.value == "") {
    271      preference.value = selectedID;
    272    } else {
    273      arrayOfPrefs.unshift(selectedID);
    274      preference.value = arrayOfPrefs.join(",");
    275    }
    276 
    277    this._acceptLanguages[selectedID] = true;
    278    this._availableLanguages.selectedItem = null;
    279    this.onAvailableLanguageSelect();
    280 
    281    // Rebuild the available list with the added item removed...
    282    this._buildAvailableLanguageList().catch(console.error);
    283  },
    284 
    285  removeLanguage() {
    286    // Build the new preference value string.
    287    var languagesArray = [];
    288    for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
    289      var item = this._activeLanguages.childNodes[i];
    290      if (!item.selected) {
    291        languagesArray.push(item.id);
    292      } else {
    293        this._acceptLanguages[item.id] = false;
    294      }
    295    }
    296    var string = languagesArray.join(",");
    297 
    298    // Get the item to select after the remove operation completes.
    299    var selection = this._activeLanguages.selectedItems;
    300    var lastSelected = selection[selection.length - 1];
    301    var selectItem = lastSelected.nextSibling || lastSelected.previousSibling;
    302    selectItem = selectItem ? selectItem.id : null;
    303 
    304    this._selectedItemID = selectItem;
    305 
    306    // Update the preference and force a UI rebuild
    307    var preference = Preferences.getSetting("acceptLanguages");
    308    preference.value = string;
    309 
    310    this._buildAvailableLanguageList().catch(console.error);
    311  },
    312 
    313  _getLocaleName(localeCode) {
    314    if (!this._availableLanguagesList.length) {
    315      this._loadAvailableLanguages();
    316    }
    317    let languageName = "";
    318    for (var i = 0; i < this._availableLanguagesList.length; ++i) {
    319      if (localeCode == this._availableLanguagesList[i].code) {
    320        return this._availableLanguagesList[i].name;
    321      }
    322      // Try resolving the locale code without region code. Can't return
    323      // directly because there might be a perfect match later.
    324      if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) {
    325        languageName = this._availableLanguagesList[i].name;
    326      }
    327    }
    328 
    329    return languageName;
    330  },
    331 
    332  moveUp() {
    333    var selectedItem = this._activeLanguages.selectedItems[0];
    334    var previousItem = selectedItem.previousSibling;
    335 
    336    var string = "";
    337    for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
    338      var item = this._activeLanguages.childNodes[i];
    339      string += i == 0 ? "" : ",";
    340      if (item.id == previousItem.id) {
    341        string += selectedItem.id;
    342      } else if (item.id == selectedItem.id) {
    343        string += previousItem.id;
    344      } else {
    345        string += item.id;
    346      }
    347    }
    348 
    349    this._selectedItemID = selectedItem.id;
    350 
    351    // Update the preference and force a UI rebuild
    352    var preference = Preferences.getSetting("acceptLanguages");
    353    preference.value = string;
    354  },
    355 
    356  moveDown() {
    357    var selectedItem = this._activeLanguages.selectedItems[0];
    358    var nextItem = selectedItem.nextSibling;
    359 
    360    var string = "";
    361    for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
    362      var item = this._activeLanguages.childNodes[i];
    363      string += i == 0 ? "" : ",";
    364      if (item.id == nextItem.id) {
    365        string += selectedItem.id;
    366      } else if (item.id == selectedItem.id) {
    367        string += nextItem.id;
    368      } else {
    369        string += item.id;
    370      }
    371    }
    372 
    373    this._selectedItemID = selectedItem.id;
    374 
    375    // Update the preference and force a UI rebuild
    376    var preference = Preferences.getSetting("acceptLanguages");
    377    preference.value = string;
    378  },
    379 
    380  onLanguageSelect() {
    381    var upButton = document.getElementById("up");
    382    var downButton = document.getElementById("down");
    383    var removeButton = document.getElementById("remove");
    384    switch (this._activeLanguages.selectedCount) {
    385      case 0:
    386        upButton.disabled = downButton.disabled = removeButton.disabled = true;
    387        break;
    388      case 1:
    389        upButton.disabled = this._activeLanguages.selectedIndex == 0;
    390        downButton.disabled =
    391          this._activeLanguages.selectedIndex ==
    392          this._activeLanguages.childNodes.length - 1;
    393        removeButton.disabled = false;
    394        break;
    395      default:
    396        upButton.disabled = true;
    397        downButton.disabled = true;
    398        removeButton.disabled = false;
    399    }
    400  },
    401 
    402  readSpoofEnglish() {
    403    var checkbox = document.getElementById("spoofEnglish");
    404    var resistFingerprinting = Services.prefs.getBoolPref(
    405      "privacy.resistFingerprinting"
    406    );
    407    if (!resistFingerprinting) {
    408      checkbox.hidden = true;
    409      return false;
    410    }
    411 
    412    var spoofEnglish = Preferences.get("privacy.spoof_english").value;
    413    var activeLanguages = this._activeLanguages;
    414    var availableLanguages = this._availableLanguages;
    415    checkbox.hidden = false;
    416    switch (spoofEnglish) {
    417      case 1: // don't spoof intl.accept_languages
    418        activeLanguages.disabled = false;
    419        activeLanguages.selectItem(activeLanguages.firstChild);
    420        availableLanguages.disabled = false;
    421        this.onAvailableLanguageSelect();
    422        return false;
    423      case 2: // spoof intl.accept_languages
    424        activeLanguages.clearSelection();
    425        activeLanguages.disabled = true;
    426        availableLanguages.disabled = true;
    427        this.onAvailableLanguageSelect();
    428        return true;
    429      default:
    430        // will prompt for spoofing intl.accept_languages if resisting fingerprinting
    431        return false;
    432    }
    433  },
    434 
    435  writeSpoofEnglish() {
    436    return document.getElementById("spoofEnglish").checked ? 2 : 1;
    437  },
    438 };
    439 
    440 window.addEventListener("load", gLanguagesDialog);