Backed out 2 changesets (bug 1874186, bug 1822097) for causing mochitests failures...
[gecko.git] / browser / components / distribution.sys.mjs
blob369de15ab2cbb8b7e43941cfa94e1548abdfe8c8
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               let faviconURI = Services.io.newURI(item.icon);
251               lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
252                 faviconURI,
253                 item.iconData,
254                 0,
255                 Services.scriptSecurityManager.getSystemPrincipal()
256               );
258               lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
259                 Services.io.newURI(item.link),
260                 faviconURI,
261                 false,
262                 lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
263                 null,
264                 Services.scriptSecurityManager.getSystemPrincipal()
265               );
266             } catch (e) {
267               console.error(e);
268             }
269           }
271           break;
272       }
273     }
274   },
276   _newProfile: false,
277   _customizationsApplied: false,
278   applyCustomizations: function DIST_applyCustomizations() {
279     this._customizationsApplied = true;
281     if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
282       this._newProfile = true;
283     }
285     if (!this._ini) {
286       return this._checkCustomizationComplete();
287     }
289     if (!this._prefDefaultsApplied) {
290       this.applyPrefDefaults();
291     }
292   },
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"));
300     if (prefs.length) {
301       let extensionIDs = [
302         "sovetnik-yandex@yandex.ru",
303         "vb@yandex.ru",
304         "ntp-mail@corp.mail.ru",
305         "ntp-okru@corp.mail.ru",
306       ];
307       for (let extensionID of extensionIDs) {
308         let addon = await lazy.AddonManager.getAddonByID(extensionID);
309         if (addon) {
310           await addon.disable();
311         }
312       }
313       for (let pref of prefs) {
314         Services.prefs.clearUserPref(pref);
315       }
316       await this._removeDistributionBookmarks();
317     } else {
318       await this._doApplyBookmarks();
319     }
320     this._bookmarksApplied = true;
321     this._checkCustomizationComplete();
322   },
324   async _doApplyBookmarks() {
325     if (!this._ini) {
326       return;
327     }
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) {
334       return;
335     }
337     let globalPrefs = enumToObject(this._ini.getKeys("Global"));
338     if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) {
339       return;
340     }
342     let bmProcessedPref;
343     try {
344       bmProcessedPref = this._ini.getString(
345         "Global",
346         "bookmarks.initialized.pref"
347       );
348     } catch (e) {
349       bmProcessedPref =
350         "distribution." +
351         this._ini.getString("Global", "id") +
352         ".bookmarksProcessed";
353     }
355     if (Services.prefs.getBoolPref(bmProcessedPref, false)) {
356       return;
357     }
359     let { ProfileAge } = ChromeUtils.importESModule(
360       "resource://gre/modules/ProfileAge.sys.mjs"
361     );
362     let profileAge = await ProfileAge();
363     let resetDate = await profileAge.reset;
365     // If the profile has been reset, don't recreate bookmarks.
366     if (!resetDate) {
367       if (sections.BookmarksMenu) {
368         await this._parseBookmarksSection(
369           lazy.PlacesUtils.bookmarks.menuGuid,
370           "BookmarksMenu"
371         );
372       }
373       if (sections.BookmarksToolbar) {
374         await this._parseBookmarksSection(
375           lazy.PlacesUtils.bookmarks.toolbarGuid,
376           "BookmarksToolbar"
377         );
378       }
379     }
380     Services.prefs.setBoolPref(bmProcessedPref, true);
381   },
383   _prefDefaultsApplied: false,
384   applyPrefDefaults: function DIST_applyPrefDefaults() {
385     this._prefDefaultsApplied = true;
386     if (!this._ini) {
387       return this._checkCustomizationComplete();
388     }
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();
395     }
396     let globalPrefs = enumToObject(this._ini.getKeys("Global"));
397     if (!(globalPrefs.id && globalPrefs.version)) {
398       return this._checkCustomizationComplete();
399     }
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();
404     }
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);
413     if (
414       distroID.startsWith("yandex") ||
415       distroID.startsWith("mailru") ||
416       distroID.startsWith("okru")
417     ) {
418       this.__defineGetter__("_ini", () => null);
419       return this._checkCustomizationComplete();
420     }
422     defaults.setStringPref(
423       "distribution.version",
424       this._ini.getString("Global", "version")
425     );
427     let partnerAbout;
428     try {
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);
433       } else {
434         partnerAbout = this._ini.getString("Global", "about");
435       }
436       defaults.setStringPref("distribution.about", partnerAbout);
437     } catch (e) {
438       /* ignore bad prefs due to bug 895473 and move on */
439     }
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);
448         if (value) {
449           preferences.set(key, value);
450         }
451       }
452     }
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);
457         if (value) {
458           preferences.set(key, value);
459         } else {
460           // If something was set by Preferences, but it's empty in language,
461           // it should be removed.
462           preferences.delete(key);
463         }
464       }
465     }
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);
470         if (value) {
471           preferences.set(key, value);
472         } else {
473           // If something was set by Preferences, but it's empty in locale,
474           // it should be removed.
475           preferences.delete(key);
476         }
477       }
478     }
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);
484       try {
485         if (prefName == "general.useragent.locale") {
486           defaults.setStringPref("intl.locale.requested", prefValue);
487         } else {
488           switch (typeof prefValue) {
489             case "boolean":
490               defaults.setBoolPref(prefName, prefValue);
491               break;
492             case "number":
493               defaults.setIntPref(prefName, prefValue);
494               break;
495             case "string":
496               defaults.setStringPref(prefName, prefValue);
497               break;
498           }
499         }
500       } catch (e) {
501         /* ignore bad prefs and move on */
502       }
503     }
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.
509       try {
510         defaults.setStringPref(
511           "distribution.id",
512           defaults
513             .get("extensions.yasearch@yandex.ru.clids.vendor")
514             .replace("firefox", "yandex")
515         );
516       } catch (e) {
517         // Just use the default distribution ID.
518       }
519     }
521     let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
522       Ci.nsIPrefLocalizedString
523     );
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);
530         if (value) {
531           localizablePreferences.set(key, value);
532         }
533       }
534     }
536     if (sections["LocalizablePreferences-" + this._language]) {
537       for (let key of this._ini.getKeys(
538         "LocalizablePreferences-" + this._language
539       )) {
540         let value = this._ini.getString(
541           "LocalizablePreferences-" + this._language,
542           key
543         );
544         if (value) {
545           localizablePreferences.set(key, value);
546         } else {
547           // If something was set by Preferences, but it's empty in language,
548           // it should be removed.
549           localizablePreferences.delete(key);
550         }
551       }
552     }
554     if (sections["LocalizablePreferences-" + this._locale]) {
555       for (let key of this._ini.getKeys(
556         "LocalizablePreferences-" + this._locale
557       )) {
558         let value = this._ini.getString(
559           "LocalizablePreferences-" + this._locale,
560           key
561         );
562         if (value) {
563           localizablePreferences.set(key, value);
564         } else {
565           // If something was set by Preferences, but it's empty in locale,
566           // it should be removed.
567           localizablePreferences.delete(key);
568         }
569       }
570     }
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;
577       try {
578         defaults.setComplexValue(
579           prefName,
580           Ci.nsIPrefLocalizedString,
581           localizedStr
582         );
583       } catch (e) {
584         /* ignore bad prefs and move on */
585       }
586     }
588     return this._checkCustomizationComplete();
589   },
591   _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
592     const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
594     if (this._newProfile) {
595       try {
596         var showPersonalToolbar = Services.prefs.getBoolPref(
597           "browser.showPersonalToolbar"
598         );
599         if (showPersonalToolbar) {
600           Services.prefs.setCharPref(
601             "browser.toolbars.bookmarks.visibility",
602             "always"
603           );
604         }
605       } catch (e) {}
606       try {
607         var showMenubar = Services.prefs.getBoolPref("browser.showMenubar");
608         if (showMenubar) {
609           Services.xulStore.setValue(
610             BROWSER_DOCURL,
611             "toolbar-menubar",
612             "autohide",
613             "false"
614           );
615         }
616       } catch (e) {}
617     }
619     let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
620     if (
621       this._customizationsApplied &&
622       this._bookmarksApplied &&
623       prefDefaultsApplied
624     ) {
625       Services.obs.notifyObservers(
626         null,
627         DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
628       );
629     }
630   },
633 function parseValue(value) {
634   try {
635     value = JSON.parse(value);
636   } catch (e) {
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(/"$/, "");
642   }
643   return value;
646 function enumToObject(UTF8Enumerator) {
647   let ret = {};
648   for (let i of UTF8Enumerator) {
649     ret[i] = 1;
650   }
651   return ret;