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/. */
5 const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
6 "distribution-customization-complete";
8 const PREF_CACHED_FILE_EXISTENCE = "distribution.iniFile.exists.value";
9 const PREF_CACHED_FILE_APPVERSION = "distribution.iniFile.exists.appversion";
11 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
14 ChromeUtils.defineESModuleGetters(lazy, {
15 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
16 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
19 export function DistributionCustomizer() {}
21 DistributionCustomizer.prototype = {
22 // These prefixes must only contain characters
23 // allowed by PlacesUtils.isValidGuid
24 BOOKMARK_GUID_PREFIX: "DstB-",
25 FOLDER_GUID_PREFIX: "DstF-",
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",
37 iniFile = loadFromProfile
38 ? Services.dirsvc.get("ProfD", Ci.nsIFile)
39 : Services.dirsvc.get("XREAppDist", Ci.nsIFile);
40 if (loadFromProfile) {
41 iniFile.leafName = "distribution";
43 iniFile.append("distribution.ini");
46 this.__defineGetter__("_iniFile", () => iniFile);
50 get _hasDistributionIni() {
51 if (Services.prefs.prefHasUserValue(PREF_CACHED_FILE_EXISTENCE)) {
52 let knownForVersion = Services.prefs.getStringPref(
53 PREF_CACHED_FILE_APPVERSION,
56 // StartupCacheInfo isn't available in xpcshell tests.
58 knownForVersion == AppConstants.MOZ_APP_VERSION &&
60 Cc["@mozilla.org/startupcacheinfo;1"].getService(
61 Ci.nsIStartupCacheInfo
62 ).FoundDiskCacheOnInit)
64 return Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE);
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
75 this.__defineGetter__("_hasDistributionIni", () => fileExists);
82 if (this._hasDistributionIni) {
83 ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
84 .getService(Ci.nsIINIParserFactory)
85 .createINIParser(this._iniFile);
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);
95 // Unable to parse INI.
96 console.error("Unable to parse distribution.ini");
99 this.__defineGetter__("_ini", () => ini);
104 const locale = Services.locale.requestedLocale || "en-US";
105 this.__defineGetter__("_locale", () => locale);
110 let language = this._locale.split("-")[0];
111 this.__defineGetter__("_language", () => language);
112 return this._language;
115 async _removeDistributionBookmarks() {
116 await lazy.PlacesUtils.bookmarks.fetch(
117 { guidPrefix: this.BOOKMARK_GUID_PREFIX },
118 bookmark => lazy.PlacesUtils.bookmarks.remove(bookmark).catch()
120 await lazy.PlacesUtils.bookmarks.fetch(
121 { guidPrefix: this.FOLDER_GUID_PREFIX },
123 lazy.PlacesUtils.bookmarks.remove(folder).catch();
128 async _parseBookmarksSection(parentGuid, section) {
129 let keys = Array.from(this._ini.getKeys(section)).sort();
130 let re = /^item\.(\d+)\.(\w+)\.?(\w*)/;
132 let defaultIndex = -1;
135 for (let key of keys) {
136 let m = re.exec(key);
138 let [, itemIndex, iprop, ilocale] = m;
139 itemIndex = parseInt(itemIndex);
145 if (keys.includes(key + "." + this._locale)) {
146 key += "." + this._locale;
147 } else if (keys.includes(key + "." + this._language)) {
148 key += "." + this._language;
151 if (!items[itemIndex]) {
152 items[itemIndex] = {};
154 items[itemIndex][iprop] = this._ini.getString(section, key);
156 if (iprop == "type" && items[itemIndex].type == "default") {
157 defaultIndex = itemIndex;
160 if (maxIndex < itemIndex) {
161 maxIndex = itemIndex;
164 dump(`Key did not match: ${key}\n`);
168 let prependIndex = 0;
169 for (let itemIndex = 0; itemIndex <= maxIndex; itemIndex++) {
170 if (!items[itemIndex]) {
174 let index = lazy.PlacesUtils.bookmarks.DEFAULT_INDEX;
175 let item = items[itemIndex];
182 if (itemIndex < defaultIndex) {
183 index = prependIndex++;
186 let folder = await lazy.PlacesUtils.bookmarks.insert({
187 type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
188 guid: lazy.PlacesUtils.generateGuidWithPrefix(
189 this.FOLDER_GUID_PREFIX
196 await this._parseBookmarksSection(
198 "BookmarksFolder-" + item.folderId
203 if (itemIndex < defaultIndex) {
204 index = prependIndex++;
207 await lazy.PlacesUtils.bookmarks.insert({
208 type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR,
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) {
220 if (itemIndex < defaultIndex) {
221 index = prependIndex++;
224 await lazy.PlacesUtils.bookmarks.insert({
234 if (itemIndex < defaultIndex) {
235 index = prependIndex++;
238 await lazy.PlacesUtils.bookmarks.insert({
239 guid: lazy.PlacesUtils.generateGuidWithPrefix(
240 this.BOOKMARK_GUID_PREFIX
248 if (item.icon && item.iconData) {
250 lazy.PlacesUtils.favicons.setFaviconForPage(
251 Services.io.newURI(item.link),
252 Services.io.newURI(item.icon),
253 Services.io.newURI(item.iconData)
266 _customizationsApplied: false,
267 applyCustomizations: function DIST_applyCustomizations() {
268 this._customizationsApplied = true;
270 if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
271 this._newProfile = true;
275 return this._checkCustomizationComplete();
278 if (!this._prefDefaultsApplied) {
279 this.applyPrefDefaults();
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"));
291 "sovetnik-yandex@yandex.ru",
293 "ntp-mail@corp.mail.ru",
294 "ntp-okru@corp.mail.ru",
296 for (let extensionID of extensionIDs) {
297 let addon = await lazy.AddonManager.getAddonByID(extensionID);
299 await addon.disable();
302 for (let pref of prefs) {
303 Services.prefs.clearUserPref(pref);
305 await this._removeDistributionBookmarks();
307 await this._doApplyBookmarks();
309 this._bookmarksApplied = true;
310 this._checkCustomizationComplete();
313 async _doApplyBookmarks() {
318 let sections = enumToObject(this._ini.getSections());
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) {
326 let globalPrefs = enumToObject(this._ini.getKeys("Global"));
327 if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) {
333 bmProcessedPref = this._ini.getString(
335 "bookmarks.initialized.pref"
340 this._ini.getString("Global", "id") +
341 ".bookmarksProcessed";
344 if (Services.prefs.getBoolPref(bmProcessedPref, false)) {
348 let { ProfileAge } = ChromeUtils.importESModule(
349 "resource://gre/modules/ProfileAge.sys.mjs"
351 let profileAge = await ProfileAge();
352 let resetDate = await profileAge.reset;
354 // If the profile has been reset, don't recreate bookmarks.
356 if (sections.BookmarksMenu) {
357 await this._parseBookmarksSection(
358 lazy.PlacesUtils.bookmarks.menuGuid,
362 if (sections.BookmarksToolbar) {
363 await this._parseBookmarksSection(
364 lazy.PlacesUtils.bookmarks.toolbarGuid,
369 Services.prefs.setBoolPref(bmProcessedPref, true);
372 _prefDefaultsApplied: false,
373 applyPrefDefaults: function DIST_applyPrefDefaults() {
374 this._prefDefaultsApplied = true;
376 return this._checkCustomizationComplete();
379 let sections = enumToObject(this._ini.getSections());
381 // The global section, and several of its fields, is required
382 if (!sections.Global) {
383 return this._checkCustomizationComplete();
385 let globalPrefs = enumToObject(this._ini.getKeys("Global"));
386 if (!(globalPrefs.id && globalPrefs.version)) {
387 return this._checkCustomizationComplete();
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();
395 let defaults = Services.prefs.getDefaultBranch(null);
397 // Global really contains info we set as prefs. They're only
398 // separate because they are "special" (read: required)
400 defaults.setStringPref("distribution.id", distroID);
403 distroID.startsWith("yandex") ||
404 distroID.startsWith("mailru") ||
405 distroID.startsWith("okru")
407 this.__defineGetter__("_ini", () => null);
408 return this._checkCustomizationComplete();
411 defaults.setStringPref(
412 "distribution.version",
413 this._ini.getString("Global", "version")
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);
423 partnerAbout = this._ini.getString("Global", "about");
425 defaults.setStringPref("distribution.about", partnerAbout);
427 /* ignore bad prefs due to bug 895473 and move on */
430 /* order of precedence is locale->language->default */
432 let preferences = new Map();
434 if (sections.Preferences) {
435 for (let key of this._ini.getKeys("Preferences")) {
436 let value = this._ini.getString("Preferences", key);
438 preferences.set(key, value);
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);
447 preferences.set(key, value);
449 // If something was set by Preferences, but it's empty in language,
450 // it should be removed.
451 preferences.delete(key);
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);
460 preferences.set(key, value);
462 // If something was set by Preferences, but it's empty in locale,
463 // it should be removed.
464 preferences.delete(key);
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);
474 if (prefName == "general.useragent.locale") {
475 defaults.setStringPref("intl.locale.requested", prefValue);
477 switch (typeof prefValue) {
479 defaults.setBoolPref(prefName, prefValue);
482 defaults.setIntPref(prefName, prefValue);
485 defaults.setStringPref(prefName, prefValue);
490 /* ignore bad prefs and move on */
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.
499 defaults.setStringPref(
502 .get("extensions.yasearch@yandex.ru.clids.vendor")
503 .replace("firefox", "yandex")
506 // Just use the default distribution ID.
510 let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
511 Ci.nsIPrefLocalizedString
514 let localizablePreferences = new Map();
516 if (sections.LocalizablePreferences) {
517 for (let key of this._ini.getKeys("LocalizablePreferences")) {
518 let value = this._ini.getString("LocalizablePreferences", key);
520 localizablePreferences.set(key, value);
525 if (sections["LocalizablePreferences-" + this._language]) {
526 for (let key of this._ini.getKeys(
527 "LocalizablePreferences-" + this._language
529 let value = this._ini.getString(
530 "LocalizablePreferences-" + this._language,
534 localizablePreferences.set(key, value);
536 // If something was set by Preferences, but it's empty in language,
537 // it should be removed.
538 localizablePreferences.delete(key);
543 if (sections["LocalizablePreferences-" + this._locale]) {
544 for (let key of this._ini.getKeys(
545 "LocalizablePreferences-" + this._locale
547 let value = this._ini.getString(
548 "LocalizablePreferences-" + this._locale,
552 localizablePreferences.set(key, value);
554 // If something was set by Preferences, but it's empty in locale,
555 // it should be removed.
556 localizablePreferences.delete(key);
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;
567 defaults.setComplexValue(
569 Ci.nsIPrefLocalizedString,
573 /* ignore bad prefs and move on */
577 return this._checkCustomizationComplete();
580 _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
581 const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
583 if (this._newProfile) {
585 var showPersonalToolbar = Services.prefs.getBoolPref(
586 "browser.showPersonalToolbar"
588 if (showPersonalToolbar) {
589 Services.prefs.setCharPref(
590 "browser.toolbars.bookmarks.visibility",
596 var showMenubar = Services.prefs.getBoolPref("browser.showMenubar");
598 Services.xulStore.setValue(
608 let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
610 this._customizationsApplied &&
611 this._bookmarksApplied &&
614 Services.obs.notifyObservers(
616 DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
622 function parseValue(value) {
624 value = JSON.parse(value);
626 // JSON.parse catches numbers and booleans.
627 // Anything else, we assume is a string.
628 // Remove the quotes that aren't needed anymore.
629 value = value.replace(/^"/, "");
630 value = value.replace(/"$/, "");
635 function enumToObject(UTF8Enumerator) {
637 for (let i of UTF8Enumerator) {