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 let faviconURI = Services.io.newURI(item.icon);
251 lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
255 Services.scriptSecurityManager.getSystemPrincipal()
258 lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
259 Services.io.newURI(item.link),
262 lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
264 Services.scriptSecurityManager.getSystemPrincipal()
277 _customizationsApplied: false,
278 applyCustomizations: function DIST_applyCustomizations() {
279 this._customizationsApplied = true;
281 if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
282 this._newProfile = true;
286 return this._checkCustomizationComplete();
289 if (!this._prefDefaultsApplied) {
290 this.applyPrefDefaults();
294 _bookmarksApplied: false,
295 async applyBookmarks() {
296 let prefs = Services.prefs
297 .getChildList("distribution.yandex")
298 .concat(Services.prefs.getChildList("distribution.mailru"))
299 .concat(Services.prefs.getChildList("distribution.okru"));
302 "sovetnik-yandex@yandex.ru",
304 "ntp-mail@corp.mail.ru",
305 "ntp-okru@corp.mail.ru",
307 for (let extensionID of extensionIDs) {
308 let addon = await lazy.AddonManager.getAddonByID(extensionID);
310 await addon.disable();
313 for (let pref of prefs) {
314 Services.prefs.clearUserPref(pref);
316 await this._removeDistributionBookmarks();
318 await this._doApplyBookmarks();
320 this._bookmarksApplied = true;
321 this._checkCustomizationComplete();
324 async _doApplyBookmarks() {
329 let sections = enumToObject(this._ini.getSections());
331 // The global section, and several of its fields, is required
332 // (we also check here to be consistent with applyPrefDefaults below)
333 if (!sections.Global) {
337 let globalPrefs = enumToObject(this._ini.getKeys("Global"));
338 if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) {
344 bmProcessedPref = this._ini.getString(
346 "bookmarks.initialized.pref"
351 this._ini.getString("Global", "id") +
352 ".bookmarksProcessed";
355 if (Services.prefs.getBoolPref(bmProcessedPref, false)) {
359 let { ProfileAge } = ChromeUtils.importESModule(
360 "resource://gre/modules/ProfileAge.sys.mjs"
362 let profileAge = await ProfileAge();
363 let resetDate = await profileAge.reset;
365 // If the profile has been reset, don't recreate bookmarks.
367 if (sections.BookmarksMenu) {
368 await this._parseBookmarksSection(
369 lazy.PlacesUtils.bookmarks.menuGuid,
373 if (sections.BookmarksToolbar) {
374 await this._parseBookmarksSection(
375 lazy.PlacesUtils.bookmarks.toolbarGuid,
380 Services.prefs.setBoolPref(bmProcessedPref, true);
383 _prefDefaultsApplied: false,
384 applyPrefDefaults: function DIST_applyPrefDefaults() {
385 this._prefDefaultsApplied = true;
387 return this._checkCustomizationComplete();
390 let sections = enumToObject(this._ini.getSections());
392 // The global section, and several of its fields, is required
393 if (!sections.Global) {
394 return this._checkCustomizationComplete();
396 let globalPrefs = enumToObject(this._ini.getKeys("Global"));
397 if (!(globalPrefs.id && globalPrefs.version)) {
398 return this._checkCustomizationComplete();
400 let distroID = this._ini.getString("Global", "id");
401 if (!globalPrefs.about && !distroID.startsWith("mozilla-")) {
402 // About is required unless it is a mozilla distro.
403 return this._checkCustomizationComplete();
406 let defaults = Services.prefs.getDefaultBranch(null);
408 // Global really contains info we set as prefs. They're only
409 // separate because they are "special" (read: required)
411 defaults.setStringPref("distribution.id", distroID);
414 distroID.startsWith("yandex") ||
415 distroID.startsWith("mailru") ||
416 distroID.startsWith("okru")
418 this.__defineGetter__("_ini", () => null);
419 return this._checkCustomizationComplete();
422 defaults.setStringPref(
423 "distribution.version",
424 this._ini.getString("Global", "version")
429 if (globalPrefs["about." + this._locale]) {
430 partnerAbout = this._ini.getString("Global", "about." + this._locale);
431 } else if (globalPrefs["about." + this._language]) {
432 partnerAbout = this._ini.getString("Global", "about." + this._language);
434 partnerAbout = this._ini.getString("Global", "about");
436 defaults.setStringPref("distribution.about", partnerAbout);
438 /* ignore bad prefs due to bug 895473 and move on */
441 /* order of precedence is locale->language->default */
443 let preferences = new Map();
445 if (sections.Preferences) {
446 for (let key of this._ini.getKeys("Preferences")) {
447 let value = this._ini.getString("Preferences", key);
449 preferences.set(key, value);
454 if (sections["Preferences-" + this._language]) {
455 for (let key of this._ini.getKeys("Preferences-" + this._language)) {
456 let value = this._ini.getString("Preferences-" + this._language, key);
458 preferences.set(key, value);
460 // If something was set by Preferences, but it's empty in language,
461 // it should be removed.
462 preferences.delete(key);
467 if (sections["Preferences-" + this._locale]) {
468 for (let key of this._ini.getKeys("Preferences-" + this._locale)) {
469 let value = this._ini.getString("Preferences-" + this._locale, key);
471 preferences.set(key, value);
473 // If something was set by Preferences, but it's empty in locale,
474 // it should be removed.
475 preferences.delete(key);
480 for (let [prefName, prefValue] of preferences) {
481 prefValue = prefValue.replace(/%LOCALE%/g, this._locale);
482 prefValue = prefValue.replace(/%LANGUAGE%/g, this._language);
483 prefValue = parseValue(prefValue);
485 if (prefName == "general.useragent.locale") {
486 defaults.setStringPref("intl.locale.requested", prefValue);
488 switch (typeof prefValue) {
490 defaults.setBoolPref(prefName, prefValue);
493 defaults.setIntPref(prefName, prefValue);
496 defaults.setStringPref(prefName, prefValue);
501 /* ignore bad prefs and move on */
505 if (this._ini.getString("Global", "id") == "yandex") {
506 // All yandex distributions have the same distribution ID,
507 // so we're using an internal preference to name them correctly.
508 // This is needed for search to work properly.
510 defaults.setStringPref(
513 .get("extensions.yasearch@yandex.ru.clids.vendor")
514 .replace("firefox", "yandex")
517 // Just use the default distribution ID.
521 let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
522 Ci.nsIPrefLocalizedString
525 let localizablePreferences = new Map();
527 if (sections.LocalizablePreferences) {
528 for (let key of this._ini.getKeys("LocalizablePreferences")) {
529 let value = this._ini.getString("LocalizablePreferences", key);
531 localizablePreferences.set(key, value);
536 if (sections["LocalizablePreferences-" + this._language]) {
537 for (let key of this._ini.getKeys(
538 "LocalizablePreferences-" + this._language
540 let value = this._ini.getString(
541 "LocalizablePreferences-" + this._language,
545 localizablePreferences.set(key, value);
547 // If something was set by Preferences, but it's empty in language,
548 // it should be removed.
549 localizablePreferences.delete(key);
554 if (sections["LocalizablePreferences-" + this._locale]) {
555 for (let key of this._ini.getKeys(
556 "LocalizablePreferences-" + this._locale
558 let value = this._ini.getString(
559 "LocalizablePreferences-" + this._locale,
563 localizablePreferences.set(key, value);
565 // If something was set by Preferences, but it's empty in locale,
566 // it should be removed.
567 localizablePreferences.delete(key);
572 for (let [prefName, prefValue] of localizablePreferences) {
573 prefValue = parseValue(prefValue);
574 prefValue = prefValue.replace(/%LOCALE%/g, this._locale);
575 prefValue = prefValue.replace(/%LANGUAGE%/g, this._language);
576 localizedStr.data = "data:text/plain," + prefName + "=" + prefValue;
578 defaults.setComplexValue(
580 Ci.nsIPrefLocalizedString,
584 /* ignore bad prefs and move on */
588 return this._checkCustomizationComplete();
591 _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
592 const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
594 if (this._newProfile) {
596 var showPersonalToolbar = Services.prefs.getBoolPref(
597 "browser.showPersonalToolbar"
599 if (showPersonalToolbar) {
600 Services.prefs.setCharPref(
601 "browser.toolbars.bookmarks.visibility",
607 var showMenubar = Services.prefs.getBoolPref("browser.showMenubar");
609 Services.xulStore.setValue(
619 let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
621 this._customizationsApplied &&
622 this._bookmarksApplied &&
625 Services.obs.notifyObservers(
627 DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
633 function parseValue(value) {
635 value = JSON.parse(value);
637 // JSON.parse catches numbers and booleans.
638 // Anything else, we assume is a string.
639 // Remove the quotes that aren't needed anymore.
640 value = value.replace(/^"/, "");
641 value = value.replace(/"$/, "");
646 function enumToObject(UTF8Enumerator) {
648 for (let i of UTF8Enumerator) {