diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/distribution.js | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/browser/components/distribution.js b/browser/components/distribution.js new file mode 100644 index 0000000000..2f5326d85a --- /dev/null +++ b/browser/components/distribution.js @@ -0,0 +1,648 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["DistributionCustomizer"]; + +const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = + "distribution-customization-complete"; + +const PREF_CACHED_FILE_EXISTENCE = "distribution.iniFile.exists.value"; +const PREF_CACHED_FILE_APPVERSION = "distribution.iniFile.exists.appversion"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +function DistributionCustomizer() {} + +DistributionCustomizer.prototype = { + // These prefixes must only contain characters + // allowed by PlacesUtils.isValidGuid + BOOKMARK_GUID_PREFIX: "DstB-", + FOLDER_GUID_PREFIX: "DstF-", + + get _iniFile() { + // For parallel xpcshell testing purposes allow loading the distribution.ini + // file from the profile folder through an hidden pref. + let loadFromProfile = Services.prefs.getBoolPref( + "distribution.testing.loadFromProfile", + false + ); + + let iniFile; + try { + iniFile = loadFromProfile + ? Services.dirsvc.get("ProfD", Ci.nsIFile) + : Services.dirsvc.get("XREAppDist", Ci.nsIFile); + if (loadFromProfile) { + iniFile.leafName = "distribution"; + } + iniFile.append("distribution.ini"); + } catch (ex) {} + + this.__defineGetter__("_iniFile", () => iniFile); + return iniFile; + }, + + get _hasDistributionIni() { + if (Services.prefs.prefHasUserValue(PREF_CACHED_FILE_EXISTENCE)) { + let knownForVersion = Services.prefs.getStringPref( + PREF_CACHED_FILE_APPVERSION, + "unknown" + ); + if (knownForVersion == AppConstants.MOZ_APP_VERSION) { + return Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE); + } + } + + let fileExists = this._iniFile.exists(); + Services.prefs.setBoolPref(PREF_CACHED_FILE_EXISTENCE, fileExists); + Services.prefs.setStringPref( + PREF_CACHED_FILE_APPVERSION, + AppConstants.MOZ_APP_VERSION + ); + + this.__defineGetter__("_hasDistributionIni", () => fileExists); + return fileExists; + }, + + get _ini() { + let ini = null; + try { + if (this._hasDistributionIni) { + ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] + .getService(Ci.nsIINIParserFactory) + .createINIParser(this._iniFile); + } + } catch (e) { + if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + // We probably had cached the file existence as true, + // but it no longer exists. We could set the new cache + // value here, but let's just invalidate the cache and + // let it be cached by a single code path on the next check. + Services.prefs.clearUserPref(PREF_CACHED_FILE_EXISTENCE); + } else { + // Unable to parse INI. + console.error("Unable to parse distribution.ini"); + } + } + this.__defineGetter__("_ini", () => ini); + return this._ini; + }, + + get _locale() { + const locale = Services.locale.requestedLocale || "en-US"; + this.__defineGetter__("_locale", () => locale); + return this._locale; + }, + + get _language() { + let language = this._locale.split("-")[0]; + this.__defineGetter__("_language", () => language); + return this._language; + }, + + async _removeDistributionBookmarks() { + await lazy.PlacesUtils.bookmarks.fetch( + { guidPrefix: this.BOOKMARK_GUID_PREFIX }, + bookmark => lazy.PlacesUtils.bookmarks.remove(bookmark).catch() + ); + await lazy.PlacesUtils.bookmarks.fetch( + { guidPrefix: this.FOLDER_GUID_PREFIX }, + folder => { + lazy.PlacesUtils.bookmarks.remove(folder).catch(); + } + ); + }, + + async _parseBookmarksSection(parentGuid, section) { + let keys = Array.from(this._ini.getKeys(section)).sort(); + let re = /^item\.(\d+)\.(\w+)\.?(\w*)/; + let items = {}; + let defaultIndex = -1; + let maxIndex = -1; + + for (let key of keys) { + let m = re.exec(key); + if (m) { + let [, itemIndex, iprop, ilocale] = m; + itemIndex = parseInt(itemIndex); + + if (ilocale) { + continue; + } + + if (keys.includes(key + "." + this._locale)) { + key += "." + this._locale; + } else if (keys.includes(key + "." + this._language)) { + key += "." + this._language; + } + + if (!items[itemIndex]) { + items[itemIndex] = {}; + } + items[itemIndex][iprop] = this._ini.getString(section, key); + + if (iprop == "type" && items[itemIndex].type == "default") { + defaultIndex = itemIndex; + } + + if (maxIndex < itemIndex) { + maxIndex = itemIndex; + } + } else { + dump(`Key did not match: ${key}\n`); + } + } + + let prependIndex = 0; + for (let itemIndex = 0; itemIndex <= maxIndex; itemIndex++) { + if (!items[itemIndex]) { + continue; + } + + let index = lazy.PlacesUtils.bookmarks.DEFAULT_INDEX; + let item = items[itemIndex]; + + switch (item.type) { + case "default": + break; + + case "folder": + if (itemIndex < defaultIndex) { + index = prependIndex++; + } + + let folder = await lazy.PlacesUtils.bookmarks.insert({ + type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + guid: lazy.PlacesUtils.generateGuidWithPrefix( + this.FOLDER_GUID_PREFIX + ), + parentGuid, + index, + title: item.title, + }); + + await this._parseBookmarksSection( + folder.guid, + "BookmarksFolder-" + item.folderId + ); + break; + + case "separator": + if (itemIndex < defaultIndex) { + index = prependIndex++; + } + + await lazy.PlacesUtils.bookmarks.insert({ + type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid, + index, + }); + break; + + case "livemark": + // Livemarks are no more supported, instead of a livemark we'll insert + // a bookmark pointing to the site uri, if available. + if (!item.siteLink) { + break; + } + if (itemIndex < defaultIndex) { + index = prependIndex++; + } + + await lazy.PlacesUtils.bookmarks.insert({ + parentGuid, + index, + title: item.title, + url: item.siteLink, + }); + break; + + case "bookmark": + default: + if (itemIndex < defaultIndex) { + index = prependIndex++; + } + + await lazy.PlacesUtils.bookmarks.insert({ + guid: lazy.PlacesUtils.generateGuidWithPrefix( + this.BOOKMARK_GUID_PREFIX + ), + parentGuid, + index, + title: item.title, + url: item.link, + }); + + if (item.icon && item.iconData) { + try { + let faviconURI = Services.io.newURI(item.icon); + lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL( + faviconURI, + item.iconData, + 0, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( + Services.io.newURI(item.link), + faviconURI, + false, + lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } catch (e) { + console.error(e); + } + } + + break; + } + } + }, + + _newProfile: false, + _customizationsApplied: false, + applyCustomizations: function DIST_applyCustomizations() { + this._customizationsApplied = true; + + if (!Services.prefs.prefHasUserValue("browser.migration.version")) { + this._newProfile = true; + } + + if (!this._ini) { + return this._checkCustomizationComplete(); + } + + if (!this._prefDefaultsApplied) { + this.applyPrefDefaults(); + } + }, + + _bookmarksApplied: false, + async applyBookmarks() { + let prefs = Services.prefs + .getChildList("distribution.yandex") + .concat(Services.prefs.getChildList("distribution.mailru")) + .concat(Services.prefs.getChildList("distribution.okru")); + if (prefs.length) { + let extensionIDs = [ + "sovetnik-yandex@yandex.ru", + "vb@yandex.ru", + "ntp-mail@corp.mail.ru", + "ntp-okru@corp.mail.ru", + ]; + for (let extensionID of extensionIDs) { + let addon = await lazy.AddonManager.getAddonByID(extensionID); + if (addon) { + await addon.disable(); + } + } + for (let pref of prefs) { + Services.prefs.clearUserPref(pref); + } + await this._removeDistributionBookmarks(); + } else { + await this._doApplyBookmarks(); + } + this._bookmarksApplied = true; + this._checkCustomizationComplete(); + }, + + async _doApplyBookmarks() { + if (!this._ini) { + return; + } + + let sections = enumToObject(this._ini.getSections()); + + // The global section, and several of its fields, is required + // (we also check here to be consistent with applyPrefDefaults below) + if (!sections.Global) { + return; + } + + let globalPrefs = enumToObject(this._ini.getKeys("Global")); + if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) { + return; + } + + let bmProcessedPref; + try { + bmProcessedPref = this._ini.getString( + "Global", + "bookmarks.initialized.pref" + ); + } catch (e) { + bmProcessedPref = + "distribution." + + this._ini.getString("Global", "id") + + ".bookmarksProcessed"; + } + + if (Services.prefs.getBoolPref(bmProcessedPref, false)) { + return; + } + + let { ProfileAge } = ChromeUtils.importESModule( + "resource://gre/modules/ProfileAge.sys.mjs" + ); + let profileAge = await ProfileAge(); + let resetDate = await profileAge.reset; + + // If the profile has been reset, don't recreate bookmarks. + if (!resetDate) { + if (sections.BookmarksMenu) { + await this._parseBookmarksSection( + lazy.PlacesUtils.bookmarks.menuGuid, + "BookmarksMenu" + ); + } + if (sections.BookmarksToolbar) { + await this._parseBookmarksSection( + lazy.PlacesUtils.bookmarks.toolbarGuid, + "BookmarksToolbar" + ); + } + } + Services.prefs.setBoolPref(bmProcessedPref, true); + }, + + _prefDefaultsApplied: false, + applyPrefDefaults: function DIST_applyPrefDefaults() { + this._prefDefaultsApplied = true; + if (!this._ini) { + return this._checkCustomizationComplete(); + } + + let sections = enumToObject(this._ini.getSections()); + + // The global section, and several of its fields, is required + if (!sections.Global) { + return this._checkCustomizationComplete(); + } + let globalPrefs = enumToObject(this._ini.getKeys("Global")); + if (!(globalPrefs.id && globalPrefs.version)) { + return this._checkCustomizationComplete(); + } + let distroID = this._ini.getString("Global", "id"); + if (!globalPrefs.about && !distroID.startsWith("mozilla-")) { + // About is required unless it is a mozilla distro. + return this._checkCustomizationComplete(); + } + + let defaults = Services.prefs.getDefaultBranch(null); + + // Global really contains info we set as prefs. They're only + // separate because they are "special" (read: required) + + defaults.setStringPref("distribution.id", distroID); + + if ( + distroID.startsWith("yandex") || + distroID.startsWith("mailru") || + distroID.startsWith("okru") + ) { + this.__defineGetter__("_ini", () => null); + return this._checkCustomizationComplete(); + } + + defaults.setStringPref( + "distribution.version", + this._ini.getString("Global", "version") + ); + + let partnerAbout; + try { + if (globalPrefs["about." + this._locale]) { + partnerAbout = this._ini.getString("Global", "about." + this._locale); + } else if (globalPrefs["about." + this._language]) { + partnerAbout = this._ini.getString("Global", "about." + this._language); + } else { + partnerAbout = this._ini.getString("Global", "about"); + } + defaults.setStringPref("distribution.about", partnerAbout); + } catch (e) { + /* ignore bad prefs due to bug 895473 and move on */ + } + + /* order of precedence is locale->language->default */ + + let preferences = new Map(); + + if (sections.Preferences) { + for (let key of this._ini.getKeys("Preferences")) { + let value = this._ini.getString("Preferences", key); + if (value) { + preferences.set(key, value); + } + } + } + + if (sections["Preferences-" + this._language]) { + for (let key of this._ini.getKeys("Preferences-" + this._language)) { + let value = this._ini.getString("Preferences-" + this._language, key); + if (value) { + preferences.set(key, value); + } else { + // If something was set by Preferences, but it's empty in language, + // it should be removed. + preferences.delete(key); + } + } + } + + if (sections["Preferences-" + this._locale]) { + for (let key of this._ini.getKeys("Preferences-" + this._locale)) { + let value = this._ini.getString("Preferences-" + this._locale, key); + if (value) { + preferences.set(key, value); + } else { + // If something was set by Preferences, but it's empty in locale, + // it should be removed. + preferences.delete(key); + } + } + } + + for (let [prefName, prefValue] of preferences) { + prefValue = prefValue.replace(/%LOCALE%/g, this._locale); + prefValue = prefValue.replace(/%LANGUAGE%/g, this._language); + prefValue = parseValue(prefValue); + try { + if (prefName == "general.useragent.locale") { + defaults.setStringPref("intl.locale.requested", prefValue); + } else { + switch (typeof prefValue) { + case "boolean": + defaults.setBoolPref(prefName, prefValue); + break; + case "number": + defaults.setIntPref(prefName, prefValue); + break; + case "string": + defaults.setStringPref(prefName, prefValue); + break; + } + } + } catch (e) { + /* ignore bad prefs and move on */ + } + } + + if (this._ini.getString("Global", "id") == "yandex") { + // All yandex distributions have the same distribution ID, + // so we're using an internal preference to name them correctly. + // This is needed for search to work properly. + try { + defaults.setStringPref( + "distribution.id", + defaults + .get("extensions.yasearch@yandex.ru.clids.vendor") + .replace("firefox", "yandex") + ); + } catch (e) { + // Just use the default distribution ID. + } + } + + let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + + let localizablePreferences = new Map(); + + if (sections.LocalizablePreferences) { + for (let key of this._ini.getKeys("LocalizablePreferences")) { + let value = this._ini.getString("LocalizablePreferences", key); + if (value) { + localizablePreferences.set(key, value); + } + } + } + + if (sections["LocalizablePreferences-" + this._language]) { + for (let key of this._ini.getKeys( + "LocalizablePreferences-" + this._language + )) { + let value = this._ini.getString( + "LocalizablePreferences-" + this._language, + key + ); + if (value) { + localizablePreferences.set(key, value); + } else { + // If something was set by Preferences, but it's empty in language, + // it should be removed. + localizablePreferences.delete(key); + } + } + } + + if (sections["LocalizablePreferences-" + this._locale]) { + for (let key of this._ini.getKeys( + "LocalizablePreferences-" + this._locale + )) { + let value = this._ini.getString( + "LocalizablePreferences-" + this._locale, + key + ); + if (value) { + localizablePreferences.set(key, value); + } else { + // If something was set by Preferences, but it's empty in locale, + // it should be removed. + localizablePreferences.delete(key); + } + } + } + + for (let [prefName, prefValue] of localizablePreferences) { + prefValue = parseValue(prefValue); + prefValue = prefValue.replace(/%LOCALE%/g, this._locale); + prefValue = prefValue.replace(/%LANGUAGE%/g, this._language); + localizedStr.data = "data:text/plain," + prefName + "=" + prefValue; + try { + defaults.setComplexValue( + prefName, + Ci.nsIPrefLocalizedString, + localizedStr + ); + } catch (e) { + /* ignore bad prefs and move on */ + } + } + + return this._checkCustomizationComplete(); + }, + + _checkCustomizationComplete: function DIST__checkCustomizationComplete() { + const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; + + if (this._newProfile) { + try { + var showPersonalToolbar = Services.prefs.getBoolPref( + "browser.showPersonalToolbar" + ); + if (showPersonalToolbar) { + Services.prefs.setCharPref( + "browser.toolbars.bookmarks.visibility", + "always" + ); + } + } catch (e) {} + try { + var showMenubar = Services.prefs.getBoolPref("browser.showMenubar"); + if (showMenubar) { + Services.xulStore.setValue( + BROWSER_DOCURL, + "toolbar-menubar", + "autohide", + "false" + ); + } + } catch (e) {} + } + + let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini; + if ( + this._customizationsApplied && + this._bookmarksApplied && + prefDefaultsApplied + ) { + Services.obs.notifyObservers( + null, + DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC + ); + } + }, +}; + +function parseValue(value) { + try { + value = JSON.parse(value); + } catch (e) { + // JSON.parse catches numbers and booleans. + // Anything else, we assume is a string. + // Remove the quotes that aren't needed anymore. + value = value.replace(/^"/, ""); + value = value.replace(/"$/, ""); + } + return value; +} + +function enumToObject(UTF8Enumerator) { + let ret = {}; + for (let i of UTF8Enumerator) { + ret[i] = 1; + } + return ret; +} |