tor-browser

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

distribution.sys.mjs (20274B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
      6  "distribution-customization-complete";
      7 
      8 const PREF_CACHED_FILE_EXISTENCE = "distribution.iniFile.exists.value";
      9 const PREF_CACHED_FILE_APPVERSION = "distribution.iniFile.exists.appversion";
     10 
     11 // These prefixes must only contain characters
     12 // allowed by PlacesUtils.isValidGuid
     13 const BOOKMARK_GUID_PREFIX = "DstB-";
     14 const FOLDER_GUID_PREFIX = "DstF-";
     15 
     16 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     17 
     18 const lazy = {};
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     21  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     22 });
     23 
     24 export function DistributionCustomizer() {}
     25 
     26 DistributionCustomizer.prototype = {
     27  get _iniFile() {
     28    // For parallel xpcshell testing purposes allow loading the distribution.ini
     29    // file from the profile folder through an hidden pref.
     30    let loadFromProfile = Services.prefs.getBoolPref(
     31      "distribution.testing.loadFromProfile",
     32      false
     33    );
     34 
     35    let iniFile;
     36    try {
     37      iniFile = loadFromProfile
     38        ? Services.dirsvc.get("ProfD", Ci.nsIFile)
     39        : Services.dirsvc.get("XREAppDist", Ci.nsIFile);
     40      if (loadFromProfile) {
     41        iniFile.leafName = "distribution";
     42      }
     43      iniFile.append("distribution.ini");
     44    } catch (ex) {}
     45 
     46    this.__defineGetter__("_iniFile", () => iniFile);
     47    return iniFile;
     48  },
     49 
     50  get _hasDistributionIni() {
     51    if (Services.prefs.prefHasUserValue(PREF_CACHED_FILE_EXISTENCE)) {
     52      let knownForVersion = Services.prefs.getStringPref(
     53        PREF_CACHED_FILE_APPVERSION,
     54        "unknown"
     55      );
     56      // StartupCacheInfo isn't available in xpcshell tests.
     57      if (
     58        knownForVersion == AppConstants.MOZ_APP_VERSION &&
     59        (Cu.isInAutomation ||
     60          Cc["@mozilla.org/startupcacheinfo;1"].getService(
     61            Ci.nsIStartupCacheInfo
     62          ).FoundDiskCacheOnInit)
     63      ) {
     64        return Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE);
     65      }
     66    }
     67 
     68    let fileExists = this._iniFile.exists();
     69    Services.prefs.setBoolPref(PREF_CACHED_FILE_EXISTENCE, fileExists);
     70    Services.prefs.setStringPref(
     71      PREF_CACHED_FILE_APPVERSION,
     72      AppConstants.MOZ_APP_VERSION
     73    );
     74 
     75    this.__defineGetter__("_hasDistributionIni", () => fileExists);
     76    return fileExists;
     77  },
     78 
     79  get _ini() {
     80    let ini = null;
     81    try {
     82      if (this._hasDistributionIni) {
     83        ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
     84          .getService(Ci.nsIINIParserFactory)
     85          .createINIParser(this._iniFile);
     86      }
     87    } catch (e) {
     88      if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
     89        // We probably had cached the file existence as true,
     90        // but it no longer exists. We could set the new cache
     91        // value here, but let's just invalidate the cache and
     92        // let it be cached by a single code path on the next check.
     93        Services.prefs.clearUserPref(PREF_CACHED_FILE_EXISTENCE);
     94      } else {
     95        // Unable to parse INI.
     96        console.error("Unable to parse distribution.ini");
     97      }
     98    }
     99    this.__defineGetter__("_ini", () => ini);
    100    return this._ini;
    101  },
    102 
    103  get _locale() {
    104    const locale = Services.locale.requestedLocale || "en-US";
    105    this.__defineGetter__("_locale", () => locale);
    106    return this._locale;
    107  },
    108 
    109  get _language() {
    110    let language = this._locale.split("-")[0];
    111    this.__defineGetter__("_language", () => language);
    112    return this._language;
    113  },
    114 
    115  async _removeDistributionBookmarks() {
    116    await lazy.PlacesUtils.bookmarks.fetch(
    117      { guidPrefix: BOOKMARK_GUID_PREFIX },
    118      bookmark =>
    119        lazy.PlacesUtils.bookmarks.remove(bookmark).catch(console.error)
    120    );
    121    await lazy.PlacesUtils.bookmarks.fetch(
    122      { guidPrefix: FOLDER_GUID_PREFIX },
    123      folder => {
    124        lazy.PlacesUtils.bookmarks.remove(folder).catch(console.error);
    125      }
    126    );
    127  },
    128 
    129  async _parseBookmarksSection(parentGuid, section) {
    130    let keys = Array.from(this._ini.getKeys(section)).sort();
    131    let re = /^item\.(\d+)\.(\w+)\.?(\w*)/;
    132    let items = {};
    133    let defaultIndex = -1;
    134    let maxIndex = -1;
    135 
    136    for (let key of keys) {
    137      let m = re.exec(key);
    138      if (m) {
    139        let [, itemIndex, iprop, ilocale] = m;
    140        itemIndex = parseInt(itemIndex);
    141 
    142        if (ilocale) {
    143          continue;
    144        }
    145 
    146        if (keys.includes(key + "." + this._locale)) {
    147          key += "." + this._locale;
    148        } else if (keys.includes(key + "." + this._language)) {
    149          key += "." + this._language;
    150        }
    151 
    152        if (!items[itemIndex]) {
    153          items[itemIndex] = {};
    154        }
    155        items[itemIndex][iprop] = this._ini.getString(section, key);
    156 
    157        if (iprop == "type" && items[itemIndex].type == "default") {
    158          defaultIndex = itemIndex;
    159        }
    160 
    161        if (maxIndex < itemIndex) {
    162          maxIndex = itemIndex;
    163        }
    164      } else {
    165        dump(`Key did not match: ${key}\n`);
    166      }
    167    }
    168 
    169    let prependIndex = 0;
    170    for (let itemIndex = 0; itemIndex <= maxIndex; itemIndex++) {
    171      if (!items[itemIndex]) {
    172        continue;
    173      }
    174 
    175      let index = lazy.PlacesUtils.bookmarks.DEFAULT_INDEX;
    176      let item = items[itemIndex];
    177 
    178      switch (item.type) {
    179        case "default":
    180          break;
    181 
    182        case "folder": {
    183          if (itemIndex < defaultIndex) {
    184            index = prependIndex++;
    185          }
    186 
    187          let folder = await lazy.PlacesUtils.bookmarks.insert({
    188            type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
    189            guid: lazy.PlacesUtils.generateGuidWithPrefix(FOLDER_GUID_PREFIX),
    190            parentGuid,
    191            index,
    192            title: item.title,
    193          });
    194 
    195          await this._parseBookmarksSection(
    196            folder.guid,
    197            "BookmarksFolder-" + item.folderId
    198          );
    199          break;
    200        }
    201 
    202        case "separator":
    203          if (itemIndex < defaultIndex) {
    204            index = prependIndex++;
    205          }
    206 
    207          await lazy.PlacesUtils.bookmarks.insert({
    208            type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR,
    209            parentGuid,
    210            index,
    211          });
    212          break;
    213 
    214        case "livemark":
    215          // Livemarks are no more supported, instead of a livemark we'll insert
    216          // a bookmark pointing to the site uri, if available.
    217          if (!item.siteLink) {
    218            break;
    219          }
    220          if (itemIndex < defaultIndex) {
    221            index = prependIndex++;
    222          }
    223 
    224          await lazy.PlacesUtils.bookmarks.insert({
    225            parentGuid,
    226            index,
    227            title: item.title,
    228            url: item.siteLink,
    229          });
    230          break;
    231 
    232        case "bookmark":
    233        default:
    234          if (itemIndex < defaultIndex) {
    235            index = prependIndex++;
    236          }
    237 
    238          await lazy.PlacesUtils.bookmarks.insert({
    239            guid: lazy.PlacesUtils.generateGuidWithPrefix(BOOKMARK_GUID_PREFIX),
    240            parentGuid,
    241            index,
    242            title: item.title,
    243            url: item.link,
    244          });
    245 
    246          if (item.icon && item.iconData) {
    247            try {
    248              lazy.PlacesUtils.favicons
    249                .setFaviconForPage(
    250                  Services.io.newURI(item.link),
    251                  Services.io.newURI(item.icon),
    252                  Services.io.newURI(item.iconData)
    253                )
    254                .catch(console.error);
    255            } catch (e) {
    256              console.error(e);
    257            }
    258          }
    259 
    260          break;
    261      }
    262    }
    263  },
    264 
    265  _newProfile: false,
    266  _customizationsApplied: false,
    267  applyCustomizations: function DIST_applyCustomizations() {
    268    this._customizationsApplied = true;
    269 
    270    if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
    271      this._newProfile = true;
    272    }
    273 
    274    if (!this._ini) {
    275      return this._checkCustomizationComplete();
    276    }
    277 
    278    if (!this._prefDefaultsApplied) {
    279      this.applyPrefDefaults();
    280    }
    281  },
    282 
    283  _bookmarksApplied: false,
    284  async applyBookmarks() {
    285    let prefs = Services.prefs
    286      .getChildList("distribution.yandex")
    287      .concat(Services.prefs.getChildList("distribution.mailru"))
    288      .concat(Services.prefs.getChildList("distribution.okru"));
    289    if (prefs.length) {
    290      let extensionIDs = [
    291        "sovetnik-yandex@yandex.ru",
    292        "vb@yandex.ru",
    293        "ntp-mail@corp.mail.ru",
    294        "ntp-okru@corp.mail.ru",
    295      ];
    296      for (let extensionID of extensionIDs) {
    297        let addon = await lazy.AddonManager.getAddonByID(extensionID);
    298        if (addon) {
    299          await addon.disable();
    300        }
    301      }
    302      for (let pref of prefs) {
    303        Services.prefs.clearUserPref(pref);
    304      }
    305      await this._removeDistributionBookmarks();
    306    } else {
    307      await this._doApplyBookmarks();
    308    }
    309    this._bookmarksApplied = true;
    310    this._checkCustomizationComplete();
    311  },
    312 
    313  async _doApplyBookmarks() {
    314    if (!this._ini) {
    315      return;
    316    }
    317 
    318    let sections = enumToObject(this._ini.getSections());
    319 
    320    // The global section, and several of its fields, is required
    321    // (we also check here to be consistent with applyPrefDefaults below)
    322    if (!sections.Global) {
    323      return;
    324    }
    325 
    326    let globalPrefs = enumToObject(this._ini.getKeys("Global"));
    327    if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) {
    328      return;
    329    }
    330 
    331    let bmProcessedPref;
    332    try {
    333      bmProcessedPref = this._ini.getString(
    334        "Global",
    335        "bookmarks.initialized.pref"
    336      );
    337    } catch (e) {
    338      bmProcessedPref =
    339        "distribution." +
    340        this._ini.getString("Global", "id") +
    341        ".bookmarksProcessed";
    342    }
    343 
    344    if (Services.prefs.getBoolPref(bmProcessedPref, false)) {
    345      return;
    346    }
    347 
    348    let { ProfileAge } = ChromeUtils.importESModule(
    349      "resource://gre/modules/ProfileAge.sys.mjs"
    350    );
    351    let profileAge = await ProfileAge();
    352    let resetDate = await profileAge.reset;
    353 
    354    // If the profile has been reset, don't recreate bookmarks.
    355    if (!resetDate) {
    356      if (sections.BookmarksMenu) {
    357        await this._parseBookmarksSection(
    358          lazy.PlacesUtils.bookmarks.menuGuid,
    359          "BookmarksMenu"
    360        );
    361      }
    362      if (sections.BookmarksToolbar) {
    363        await this._parseBookmarksSection(
    364          lazy.PlacesUtils.bookmarks.toolbarGuid,
    365          "BookmarksToolbar"
    366        );
    367      }
    368    }
    369    Services.prefs.setBoolPref(bmProcessedPref, true);
    370  },
    371 
    372  _prefDefaultsApplied: false,
    373  applyPrefDefaults: function DIST_applyPrefDefaults() {
    374    this._prefDefaultsApplied = true;
    375    if (!this._ini) {
    376      return this._checkCustomizationComplete();
    377    }
    378 
    379    let sections = enumToObject(this._ini.getSections());
    380 
    381    // The global section, and several of its fields, is required
    382    if (!sections.Global) {
    383      return this._checkCustomizationComplete();
    384    }
    385    let globalPrefs = enumToObject(this._ini.getKeys("Global"));
    386    if (!(globalPrefs.id && globalPrefs.version)) {
    387      return this._checkCustomizationComplete();
    388    }
    389    let distroID = this._ini.getString("Global", "id");
    390    if (!globalPrefs.about && !distroID.startsWith("mozilla-")) {
    391      // About is required unless it is a mozilla distro.
    392      return this._checkCustomizationComplete();
    393    }
    394 
    395    let defaults = Services.prefs.getDefaultBranch(null);
    396 
    397    // Global really contains info we set as prefs.  They're only
    398    // separate because they are "special" (read: required)
    399 
    400    defaults.setStringPref("distribution.id", distroID);
    401 
    402    if (
    403      distroID.startsWith("yandex") ||
    404      distroID.startsWith("mailru") ||
    405      distroID.startsWith("okru")
    406    ) {
    407      this.__defineGetter__("_ini", () => null);
    408      return this._checkCustomizationComplete();
    409    }
    410 
    411    defaults.setStringPref(
    412      "distribution.version",
    413      this._ini.getString("Global", "version")
    414    );
    415 
    416    let partnerAbout;
    417    try {
    418      if (globalPrefs["about." + this._locale]) {
    419        partnerAbout = this._ini.getString("Global", "about." + this._locale);
    420      } else if (globalPrefs["about." + this._language]) {
    421        partnerAbout = this._ini.getString("Global", "about." + this._language);
    422      } else {
    423        partnerAbout = this._ini.getString("Global", "about");
    424      }
    425      defaults.setStringPref("distribution.about", partnerAbout);
    426    } catch (e) {
    427      /* ignore bad prefs due to bug 895473 and move on */
    428    }
    429 
    430    /* order of precedence is locale->language->default */
    431 
    432    let preferences = new Map();
    433 
    434    if (sections.Preferences) {
    435      for (let key of this._ini.getKeys("Preferences")) {
    436        let value = this._ini.getString("Preferences", key);
    437        if (value) {
    438          preferences.set(key, value);
    439        }
    440      }
    441    }
    442 
    443    if (sections["Preferences-" + this._language]) {
    444      for (let key of this._ini.getKeys("Preferences-" + this._language)) {
    445        let value = this._ini.getString("Preferences-" + this._language, key);
    446        if (value) {
    447          preferences.set(key, value);
    448        } else {
    449          // If something was set by Preferences, but it's empty in language,
    450          // it should be removed.
    451          preferences.delete(key);
    452        }
    453      }
    454    }
    455 
    456    if (sections["Preferences-" + this._locale]) {
    457      for (let key of this._ini.getKeys("Preferences-" + this._locale)) {
    458        let value = this._ini.getString("Preferences-" + this._locale, key);
    459        if (value) {
    460          preferences.set(key, value);
    461        } else {
    462          // If something was set by Preferences, but it's empty in locale,
    463          // it should be removed.
    464          preferences.delete(key);
    465        }
    466      }
    467    }
    468 
    469    for (let [prefName, prefValue] of preferences) {
    470      prefValue = prefValue.replace(/%LOCALE%/g, this._locale);
    471      prefValue = prefValue.replace(/%LANGUAGE%/g, this._language);
    472      prefValue = parseValue(prefValue);
    473      try {
    474        if (prefName == "general.useragent.locale") {
    475          defaults.setStringPref("intl.locale.requested", prefValue);
    476        } else {
    477          switch (typeof prefValue) {
    478            case "boolean":
    479              defaults.setBoolPref(prefName, prefValue);
    480              break;
    481            case "number":
    482              defaults.setIntPref(prefName, prefValue);
    483              break;
    484            case "string":
    485              defaults.setStringPref(prefName, prefValue);
    486              break;
    487          }
    488        }
    489      } catch (e) {
    490        /* ignore bad prefs and move on */
    491      }
    492    }
    493 
    494    if (this._ini.getString("Global", "id") == "yandex") {
    495      // All yandex distributions have the same distribution ID,
    496      // so we're using an internal preference to name them correctly.
    497      // This is needed for search to work properly.
    498      try {
    499        defaults.setStringPref(
    500          "distribution.id",
    501          defaults
    502            .get("extensions.yasearch@yandex.ru.clids.vendor")
    503            .replace("firefox", "yandex")
    504        );
    505      } catch (e) {
    506        // Just use the default distribution ID.
    507      }
    508    }
    509 
    510    let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
    511      Ci.nsIPrefLocalizedString
    512    );
    513 
    514    let localizablePreferences = new Map();
    515 
    516    if (sections.LocalizablePreferences) {
    517      for (let key of this._ini.getKeys("LocalizablePreferences")) {
    518        let value = this._ini.getString("LocalizablePreferences", key);
    519        if (value) {
    520          localizablePreferences.set(key, value);
    521        }
    522      }
    523    }
    524 
    525    if (sections["LocalizablePreferences-" + this._language]) {
    526      for (let key of this._ini.getKeys(
    527        "LocalizablePreferences-" + this._language
    528      )) {
    529        let value = this._ini.getString(
    530          "LocalizablePreferences-" + this._language,
    531          key
    532        );
    533        if (value) {
    534          localizablePreferences.set(key, value);
    535        } else {
    536          // If something was set by Preferences, but it's empty in language,
    537          // it should be removed.
    538          localizablePreferences.delete(key);
    539        }
    540      }
    541    }
    542 
    543    if (sections["LocalizablePreferences-" + this._locale]) {
    544      for (let key of this._ini.getKeys(
    545        "LocalizablePreferences-" + this._locale
    546      )) {
    547        let value = this._ini.getString(
    548          "LocalizablePreferences-" + this._locale,
    549          key
    550        );
    551        if (value) {
    552          localizablePreferences.set(key, value);
    553        } else {
    554          // If something was set by Preferences, but it's empty in locale,
    555          // it should be removed.
    556          localizablePreferences.delete(key);
    557        }
    558      }
    559    }
    560 
    561    for (let [prefName, prefValue] of localizablePreferences) {
    562      prefValue = parseValue(prefValue);
    563      prefValue = prefValue.replace(/%LOCALE%/g, this._locale);
    564      prefValue = prefValue.replace(/%LANGUAGE%/g, this._language);
    565      localizedStr.data = "data:text/plain," + prefName + "=" + prefValue;
    566      try {
    567        defaults.setComplexValue(
    568          prefName,
    569          Ci.nsIPrefLocalizedString,
    570          localizedStr
    571        );
    572      } catch (e) {
    573        /* ignore bad prefs and move on */
    574      }
    575    }
    576 
    577    return this._checkCustomizationComplete();
    578  },
    579 
    580  _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
    581    const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
    582 
    583    if (this._newProfile) {
    584      try {
    585        var showPersonalToolbar = Services.prefs.getBoolPref(
    586          "browser.showPersonalToolbar"
    587        );
    588        if (showPersonalToolbar) {
    589          Services.prefs.setCharPref(
    590            "browser.toolbars.bookmarks.visibility",
    591            "always"
    592          );
    593        }
    594      } catch (e) {}
    595      try {
    596        var showMenubar = Services.prefs.getBoolPref("browser.showMenubar");
    597        if (showMenubar) {
    598          Services.xulStore.setValue(
    599            BROWSER_DOCURL,
    600            "toolbar-menubar",
    601            "autohide",
    602            "false"
    603          );
    604        }
    605      } catch (e) {}
    606      // If a theme was specified in the distribution, and it's a new profile,
    607      // set the theme as default.
    608      try {
    609        const activeThemeID = Services.prefs.getCharPref(
    610          "extensions.activeThemeID"
    611        );
    612        if (activeThemeID) {
    613          lazy.AddonManager.getAddonByID(activeThemeID).then(addon =>
    614            addon?.enable()
    615          );
    616        }
    617      } catch (e) {}
    618    }
    619 
    620    let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
    621    if (
    622      this._customizationsApplied &&
    623      this._bookmarksApplied &&
    624      prefDefaultsApplied
    625    ) {
    626      Services.obs.notifyObservers(
    627        null,
    628        DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
    629      );
    630    }
    631  },
    632 };
    633 
    634 function parseValue(value) {
    635  try {
    636    value = JSON.parse(value);
    637  } catch (e) {
    638    // JSON.parse catches numbers and booleans.
    639    // Anything else, we assume is a string.
    640    // Remove the quotes that aren't needed anymore.
    641    value = value.replace(/^"/, "");
    642    value = value.replace(/"$/, "");
    643  }
    644  return value;
    645 }
    646 
    647 function enumToObject(UTF8Enumerator) {
    648  let ret = {};
    649  for (let i of UTF8Enumerator) {
    650    ret[i] = 1;
    651  }
    652  return ret;
    653 }
    654 
    655 export let DistributionManagement = {
    656  _distributionCustomizer: null,
    657  get BOOKMARK_GUID_PREFIX() {
    658    return BOOKMARK_GUID_PREFIX;
    659  },
    660  get FOLDER_GUID_PREFIX() {
    661    return FOLDER_GUID_PREFIX;
    662  },
    663 
    664  _ensureCustomizer() {
    665    if (this._distributionCustomizer) {
    666      return;
    667    }
    668    this._distributionCustomizer = new DistributionCustomizer();
    669    Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
    670  },
    671 
    672  observe(_subject, topic) {
    673    if (topic == DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC) {
    674      Services.obs.removeObserver(
    675        this,
    676        DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
    677      );
    678      this._distributionCustomizer = null;
    679    }
    680  },
    681 
    682  applyCustomizations() {
    683    this._ensureCustomizer();
    684    this._distributionCustomizer.applyCustomizations();
    685  },
    686 
    687  applyBookmarks() {
    688    this._ensureCustomizer();
    689    this._distributionCustomizer.applyBookmarks();
    690  },
    691 
    692  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
    693 };