Backed out 2 changesets (bug 1900622) for causing Bug 1908553 and ktlint failure...
[gecko.git] / browser / components / distribution.sys.mjs
blobf58fd7a419ceae8fca629b683a0b01addef80554
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";
13 const lazy = {};
14 ChromeUtils.defineESModuleGetters(lazy, {
15   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
16   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
17 });
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-",
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     );
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) {}
46     this.__defineGetter__("_iniFile", () => iniFile);
47     return iniFile;
48   },
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     }
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     );
75     this.__defineGetter__("_hasDistributionIni", () => fileExists);
76     return fileExists;
77   },
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   },
103   get _locale() {
104     const locale = Services.locale.requestedLocale || "en-US";
105     this.__defineGetter__("_locale", () => locale);
106     return this._locale;
107   },
109   get _language() {
110     let language = this._locale.split("-")[0];
111     this.__defineGetter__("_language", () => language);
112     return this._language;
113   },
115   async _removeDistributionBookmarks() {
116     await lazy.PlacesUtils.bookmarks.fetch(
117       { guidPrefix: this.BOOKMARK_GUID_PREFIX },
118       bookmark => lazy.PlacesUtils.bookmarks.remove(bookmark).catch()
119     );
120     await lazy.PlacesUtils.bookmarks.fetch(
121       { guidPrefix: this.FOLDER_GUID_PREFIX },
122       folder => {
123         lazy.PlacesUtils.bookmarks.remove(folder).catch();
124       }
125     );
126   },
128   async _parseBookmarksSection(parentGuid, section) {
129     let keys = Array.from(this._ini.getKeys(section)).sort();
130     let re = /^item\.(\d+)\.(\w+)\.?(\w*)/;
131     let items = {};
132     let defaultIndex = -1;
133     let maxIndex = -1;
135     for (let key of keys) {
136       let m = re.exec(key);
137       if (m) {
138         let [, itemIndex, iprop, ilocale] = m;
139         itemIndex = parseInt(itemIndex);
141         if (ilocale) {
142           continue;
143         }
145         if (keys.includes(key + "." + this._locale)) {
146           key += "." + this._locale;
147         } else if (keys.includes(key + "." + this._language)) {
148           key += "." + this._language;
149         }
151         if (!items[itemIndex]) {
152           items[itemIndex] = {};
153         }
154         items[itemIndex][iprop] = this._ini.getString(section, key);
156         if (iprop == "type" && items[itemIndex].type == "default") {
157           defaultIndex = itemIndex;
158         }
160         if (maxIndex < itemIndex) {
161           maxIndex = itemIndex;
162         }
163       } else {
164         dump(`Key did not match: ${key}\n`);
165       }
166     }
168     let prependIndex = 0;
169     for (let itemIndex = 0; itemIndex <= maxIndex; itemIndex++) {
170       if (!items[itemIndex]) {
171         continue;
172       }
174       let index = lazy.PlacesUtils.bookmarks.DEFAULT_INDEX;
175       let item = items[itemIndex];
177       switch (item.type) {
178         case "default":
179           break;
181         case "folder":
182           if (itemIndex < defaultIndex) {
183             index = prependIndex++;
184           }
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
190             ),
191             parentGuid,
192             index,
193             title: item.title,
194           });
196           await this._parseBookmarksSection(
197             folder.guid,
198             "BookmarksFolder-" + item.folderId
199           );
200           break;
202         case "separator":
203           if (itemIndex < defaultIndex) {
204             index = prependIndex++;
205           }
207           await lazy.PlacesUtils.bookmarks.insert({
208             type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR,
209             parentGuid,
210             index,
211           });
212           break;
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           }
224           await lazy.PlacesUtils.bookmarks.insert({
225             parentGuid,
226             index,
227             title: item.title,
228             url: item.siteLink,
229           });
230           break;
232         case "bookmark":
233         default:
234           if (itemIndex < defaultIndex) {
235             index = prependIndex++;
236           }
238           await lazy.PlacesUtils.bookmarks.insert({
239             guid: lazy.PlacesUtils.generateGuidWithPrefix(
240               this.BOOKMARK_GUID_PREFIX
241             ),
242             parentGuid,
243             index,
244             title: item.title,
245             url: item.link,
246           });
248           if (item.icon && item.iconData) {
249             try {
250               lazy.PlacesUtils.favicons.setFaviconForPage(
251                 Services.io.newURI(item.link),
252                 Services.io.newURI(item.icon),
253                 Services.io.newURI(item.iconData)
254               );
255             } catch (e) {
256               console.error(e);
257             }
258           }
260           break;
261       }
262     }
263   },
265   _newProfile: false,
266   _customizationsApplied: false,
267   applyCustomizations: function DIST_applyCustomizations() {
268     this._customizationsApplied = true;
270     if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
271       this._newProfile = true;
272     }
274     if (!this._ini) {
275       return this._checkCustomizationComplete();
276     }
278     if (!this._prefDefaultsApplied) {
279       this.applyPrefDefaults();
280     }
281   },
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   },
313   async _doApplyBookmarks() {
314     if (!this._ini) {
315       return;
316     }
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) {
323       return;
324     }
326     let globalPrefs = enumToObject(this._ini.getKeys("Global"));
327     if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) {
328       return;
329     }
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     }
344     if (Services.prefs.getBoolPref(bmProcessedPref, false)) {
345       return;
346     }
348     let { ProfileAge } = ChromeUtils.importESModule(
349       "resource://gre/modules/ProfileAge.sys.mjs"
350     );
351     let profileAge = await ProfileAge();
352     let resetDate = await profileAge.reset;
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   },
372   _prefDefaultsApplied: false,
373   applyPrefDefaults: function DIST_applyPrefDefaults() {
374     this._prefDefaultsApplied = true;
375     if (!this._ini) {
376       return this._checkCustomizationComplete();
377     }
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();
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     }
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);
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     }
411     defaults.setStringPref(
412       "distribution.version",
413       this._ini.getString("Global", "version")
414     );
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     }
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);
437         if (value) {
438           preferences.set(key, value);
439         }
440       }
441     }
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     }
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     }
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     }
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     }
510     let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
511       Ci.nsIPrefLocalizedString
512     );
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);
519         if (value) {
520           localizablePreferences.set(key, value);
521         }
522       }
523     }
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     }
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     }
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     }
577     return this._checkCustomizationComplete();
578   },
580   _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
581     const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
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     }
608     let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
609     if (
610       this._customizationsApplied &&
611       this._bookmarksApplied &&
612       prefDefaultsApplied
613     ) {
614       Services.obs.notifyObservers(
615         null,
616         DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
617       );
618     }
619   },
622 function parseValue(value) {
623   try {
624     value = JSON.parse(value);
625   } catch (e) {
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(/"$/, "");
631   }
632   return value;
635 function enumToObject(UTF8Enumerator) {
636   let ret = {};
637   for (let i of UTF8Enumerator) {
638     ret[i] = 1;
639   }
640   return ret;