diff options
Diffstat (limited to 'toolkit/modules/UpdateUtils.sys.mjs')
-rw-r--r-- | toolkit/modules/UpdateUtils.sys.mjs | 1122 |
1 files changed, 1122 insertions, 0 deletions
diff --git a/toolkit/modules/UpdateUtils.sys.mjs b/toolkit/modules/UpdateUtils.sys.mjs new file mode 100644 index 0000000000..3a86099aa8 --- /dev/null +++ b/toolkit/modules/UpdateUtils.sys.mjs @@ -0,0 +1,1122 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", + WindowsVersionInfo: + "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs", + ctypes: "resource://gre/modules/ctypes.sys.mjs", +}); + +const PER_INSTALLATION_PREFS_PLATFORMS = ["win"]; + +// The file that stores Application Update configuration settings. The file is +// located in the update directory which makes it a common setting across all +// application profiles and allows the Background Update Agent to read it. +const FILE_UPDATE_CONFIG_JSON = "update-config.json"; +const FILE_UPDATE_LOCALE = "update.locale"; +const PREF_APP_DISTRIBUTION = "distribution.id"; +const PREF_APP_DISTRIBUTION_VERSION = "distribution.version"; + +export var UpdateUtils = { + _locale: undefined, + _configFilePath: undefined, + + /** + * Read the update channel from defaults only. We do this to ensure that + * the channel is tightly coupled with the application and does not apply + * to other instances of the application that may use the same profile. + * + * @param [optional] aIncludePartners + * Whether or not to include the partner bits. Default: true. + */ + getUpdateChannel(aIncludePartners = true) { + let defaults = Services.prefs.getDefaultBranch(null); + let channel = defaults.getCharPref( + "app.update.channel", + AppConstants.MOZ_UPDATE_CHANNEL + ); + + if (aIncludePartners) { + try { + let partners = Services.prefs.getChildList("app.partner.").sort(); + if (partners.length) { + channel += "-cck"; + partners.forEach(function (prefName) { + channel += "-" + Services.prefs.getCharPref(prefName); + }); + } + } catch (e) { + console.error(e); + } + } + + return channel; + }, + + get UpdateChannel() { + return this.getUpdateChannel(); + }, + + /** + * Formats a URL by replacing %...% values with OS, build and locale specific + * values. + * + * @param url + * The URL to format. + * @return The formatted URL. + */ + async formatUpdateURL(url) { + const locale = await this.getLocale(); + + return url + .replace(/%(\w+)%/g, (match, name) => { + switch (name) { + case "PRODUCT": + return Services.appinfo.name; + case "VERSION": + return Services.appinfo.version; + case "BUILD_ID": + return Services.appinfo.appBuildID; + case "BUILD_TARGET": + return Services.appinfo.OS + "_" + this.ABI; + case "OS_VERSION": + return this.OSVersion; + case "LOCALE": + return locale; + case "CHANNEL": + return this.UpdateChannel; + case "PLATFORM_VERSION": + return Services.appinfo.platformVersion; + case "SYSTEM_CAPABILITIES": + return getSystemCapabilities(); + case "DISTRIBUTION": + return getDistributionPrefValue(PREF_APP_DISTRIBUTION); + case "DISTRIBUTION_VERSION": + return getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION); + } + return match; + }) + .replace(/\+/g, "%2B"); + }, + + /** + * Gets the locale from the update.locale file for replacing %LOCALE% in the + * update url. The update.locale file can be located in the application + * directory or the GRE directory with preference given to it being located in + * the application directory. + */ + async getLocale() { + if (this._locale !== undefined) { + return this._locale; + } + + for (let res of ["app", "gre"]) { + const url = "resource://" + res + "/" + FILE_UPDATE_LOCALE; + let data; + try { + data = await fetch(url); + } catch (e) { + continue; + } + const locale = await data.text(); + if (locale) { + return (this._locale = locale.trim()); + } + } + + console.error( + FILE_UPDATE_LOCALE, + " file doesn't exist in either the application or GRE directories" + ); + + return (this._locale = null); + }, + + /* Get the path to the config file. */ + getConfigFilePath() { + let path = PathUtils.join( + Services.dirsvc.get("UpdRootD", Ci.nsIFile).path, + FILE_UPDATE_CONFIG_JSON + ); + return (this._configFilePath = path); + }, + + get configFilePath() { + if (this._configFilePath !== undefined) { + return this._configFilePath; + } + return this.getConfigFilePath(); + }, + + /** + * Determines whether or not the Application Update Service automatically + * downloads and installs updates. This corresponds to whether or not the user + * has selected "Automatically install updates" in about:preferences. + * + * On Windows, this setting is shared across all profiles for the installation + * and is read asynchronously from the file. On other operating systems, this + * setting is stored in a pref and is thus a per-profile setting. + * + * @return A Promise that resolves with a boolean. + */ + async getAppUpdateAutoEnabled() { + return this.readUpdateConfigSetting("app.update.auto"); + }, + + /** + * Toggles whether the Update Service automatically downloads and installs + * updates. This effectively selects between the "Automatically install + * updates" and "Check for updates but let you choose to install them" options + * in about:preferences. + * + * On Windows, this setting is shared across all profiles for the installation + * and is written asynchronously to the file. On other operating systems, this + * setting is stored in a pref and is thus a per-profile setting. + * + * If this method is called when the setting is locked, the returned promise + * will reject. The lock status can be determined with + * UpdateUtils.appUpdateAutoSettingIsLocked() + * + * @param enabled If set to true, automatic download and installation of + * updates will be enabled. If set to false, this will be + * disabled. + * @return A Promise that, once the setting has been saved, resolves with the + * boolean value that was saved. If the setting could not be + * successfully saved, the Promise will reject. + * On Windows, where this setting is stored in a file, this Promise + * may reject with an I/O error. + * On other operating systems, this promise should not reject as + * this operation simply sets a pref. + */ + async setAppUpdateAutoEnabled(enabledValue) { + return this.writeUpdateConfigSetting("app.update.auto", !!enabledValue); + }, + + /** + * This function should be used to determine if the automatic application + * update setting is locked by an enterprise policy + * + * @return true if the automatic update setting is currently locked. + * Otherwise, false. + */ + appUpdateAutoSettingIsLocked() { + return this.appUpdateSettingIsLocked("app.update.auto"); + }, + + /** + * Indicates whether or not per-installation prefs are supported on this + * platform. + */ + PER_INSTALLATION_PREFS_SUPPORTED: PER_INSTALLATION_PREFS_PLATFORMS.includes( + AppConstants.platform + ), + + /** + * Possible per-installation pref types. + */ + PER_INSTALLATION_PREF_TYPE_BOOL: "boolean", + PER_INSTALLATION_PREF_TYPE_ASCII_STRING: "ascii", + PER_INSTALLATION_PREF_TYPE_INT: "integer", + + /** + * We want the preference definitions to be part of UpdateUtils for a couple + * of reasons. It's a clean way for consumers to look up things like observer + * topic names. It also allows us to manipulate the supported prefs during + * testing. However, we want to use values out of UpdateUtils (like pref + * types) to construct this object. Therefore, this will initially be a + * placeholder, which we will properly define after the UpdateUtils object + * definition. + */ + PER_INSTALLATION_PREFS: null, + + /** + * This function initializes per-installation prefs. Note that it does not + * need to be called manually; it is already called within the file. + * + * This function is called on startup, so it does not read or write to disk. + */ + initPerInstallPrefs() { + // If we don't have per-installation prefs, we store the update config in + // preferences. In that case, the best way to notify observers of this + // setting is just to propagate it from a pref observer. This ensures that + // the expected observers still get notified, even if a user manually + // changes the pref value. + if (!UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED) { + let initialConfig = {}; + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + try { + let initialValue = prefTypeFns.getProfilePref(prefName); + initialConfig[prefName] = initialValue; + } catch (e) {} + + Services.prefs.addObserver(prefName, async (subject, topic, data) => { + let config = { ...gUpdateConfigCache }; + config[prefName] = await UpdateUtils.readUpdateConfigSetting( + prefName + ); + maybeUpdateConfigChanged(config); + }); + } + + // On the first call to maybeUpdateConfigChanged, it has nothing to + // compare its input to, so it just populates the cache and doesn't notify + // any observers. This makes sense during normal usage, because the first + // call will be on the first config file read, and we don't want to notify + // observers of changes on the first read. But that means that when + // propagating pref observers, we need to make one initial call to + // simulate that initial read so that the cache will be populated when the + // first pref observer fires. + maybeUpdateConfigChanged(initialConfig); + } + }, + + /** + * Reads an installation-specific configuration setting from the update config + * JSON file. This function is guaranteed not to throw. If there are problems + * reading the file, the default value will be returned so that update can + * proceed. This is particularly important since the configuration file is + * writable by anyone and we don't want an unprivileged user to be able to + * break update for other users. + * + * If relevant policies are active, this function will read the policy value + * rather than the stored value. + * + * @param prefName + * The preference to read. Must be a key of the + * PER_INSTALLATION_PREFS object. + * @return A Promise that resolves with the pref's value. + */ + readUpdateConfigSetting(prefName) { + if (!(prefName in this.PER_INSTALLATION_PREFS)) { + return Promise.reject( + new Error( + `UpdateUtils.readUpdateConfigSetting: Unknown per-installation ` + + `pref '${prefName}'` + ) + ); + } + + const pref = this.PER_INSTALLATION_PREFS[prefName]; + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + if (Services.policies && "policyFn" in pref) { + let policyValue = pref.policyFn(); + if (policyValue !== null) { + return Promise.resolve(policyValue); + } + } + + if (!this.PER_INSTALLATION_PREFS_SUPPORTED) { + // If we don't have per-installation prefs, we use regular preferences. + let prefValue = prefTypeFns.getProfilePref(prefName, pref.defaultValue); + return Promise.resolve(prefValue); + } + + let readPromise = updateConfigIOPromise + // All promises returned by (read|write)UpdateConfigSetting are part of a + // single promise chain in order to serialize disk operations. But we + // don't want the entire promise chain to reject when one operation fails. + // So we are going to silently clear any rejections the promise chain + // might contain. + // + // We will also pass an empty function for the first then() argument as + // well, just to make sure we are starting fresh rather than potentially + // propagating some stale value. + .then( + () => {}, + () => {} + ) + .then(readUpdateConfig) + .then(maybeUpdateConfigChanged) + .then(config => { + return readEffectiveValue(config, prefName); + }); + updateConfigIOPromise = readPromise; + return readPromise; + }, + + /** + * Changes an installation-specific configuration setting by writing it to + * the update config JSON file. + * + * If this method is called on a prefName that is locked, the returned promise + * will reject. The lock status can be determined with + * appUpdateSettingIsLocked(). + * + * @param prefName + * The preference to change. This must be a key of the + * PER_INSTALLATION_PREFS object. + * @param value + * The value to be written. Its type must match + * PER_INSTALLATION_PREFS[prefName].type + * @param options + * Optional. An object containing any of the following keys: + * setDefaultOnly + * If set to true, the default branch value will be set rather + * than user value. If a user value is set for this pref, this + * will have no effect on the pref's effective value. + * NOTE - The behavior of the default pref branch currently + * differs depending on whether the current platform + * supports per-installation prefs. If they are + * supported, default branch values persist across + * Firefox sessions. If they aren't supported, default + * branch values reset when Firefox shuts down. + * @return A Promise that, once the setting has been saved, resolves with the + * value that was saved. + * @throw If there is an I/O error when attempting to write to the config + * file, the returned Promise will reject with a DOMException. + */ + writeUpdateConfigSetting(prefName, value, options) { + if (!(prefName in this.PER_INSTALLATION_PREFS)) { + return Promise.reject( + new Error( + `UpdateUtils.writeUpdateConfigSetting: Unknown per-installation ` + + `pref '${prefName}'` + ) + ); + } + + if (this.appUpdateSettingIsLocked(prefName)) { + return Promise.reject( + new Error( + `UpdateUtils.writeUpdateConfigSetting: Unable to change value of ` + + `setting '${prefName}' because it is locked by policy` + ) + ); + } + + if (!options) { + options = {}; + } + + const pref = this.PER_INSTALLATION_PREFS[prefName]; + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + if (!prefTypeFns.isValid(value)) { + return Promise.reject( + new Error( + `UpdateUtils.writeUpdateConfigSetting: Attempted to change pref ` + + `'${prefName} to invalid value: ${JSON.stringify(value)}` + ) + ); + } + + if (!this.PER_INSTALLATION_PREFS_SUPPORTED) { + // If we don't have per-installation prefs, we use regular preferences. + if (options.setDefaultOnly) { + prefTypeFns.setProfileDefaultPref(prefName, value); + } else { + prefTypeFns.setProfilePref(prefName, value); + } + // Rather than call maybeUpdateConfigChanged, a pref observer has + // been connected to the relevant pref. This allows us to catch direct + // changes to prefs (which Firefox shouldn't be doing, but the user + // might do in about:config). + return Promise.resolve(value); + } + + let writePromise = updateConfigIOPromise + // All promises returned by (read|write)UpdateConfigSetting are part of a + // single promise chain in order to serialize disk operations. But we + // don't want the entire promise chain to reject when one operation fails. + // So we are going to silently clear any rejections the promise chain + // might contain. + // + // We will also pass an empty function for the first then() argument as + // well, just to make sure we are starting fresh rather than potentially + // propagating some stale value. + .then( + () => {}, + () => {} + ) + // We always re-read the update config before writing, rather than using a + // cached version. Otherwise, two simultaneous instances may overwrite + // each other's changes. + .then(readUpdateConfig) + .then(async config => { + setConfigValue(config, prefName, value, { + setDefaultOnly: !!options.setDefaultOnly, + }); + + try { + await writeUpdateConfig(config); + return config; + } catch (e) { + console.error( + "UpdateUtils.writeUpdateConfigSetting: App update configuration " + + "file write failed. Exception: ", + e + ); + // Re-throw the error so the caller knows that writing the value in + // the app update config file failed. + throw e; + } + }) + .then(maybeUpdateConfigChanged) + .then(() => { + // If this value wasn't written, a previous promise in the chain will + // have thrown, so we can unconditionally return the expected written + // value as the value that was written. + return value; + }); + updateConfigIOPromise = writePromise; + return writePromise; + }, + + /** + * Returns true if the specified pref is controlled by policy and thus should + * not be changeable by the user. + */ + appUpdateSettingIsLocked(prefName) { + if (!(prefName in UpdateUtils.PER_INSTALLATION_PREFS)) { + return Promise.reject( + new Error( + `UpdateUtils.appUpdateSettingIsLocked: Unknown per-installation pref '${prefName}'` + ) + ); + } + + // If we don't have policy support, nothing can be locked. + if (!Services.policies) { + return false; + } + + const pref = UpdateUtils.PER_INSTALLATION_PREFS[prefName]; + if (!pref.policyFn) { + return false; + } + const policyValue = pref.policyFn(); + return policyValue !== null; + }, +}; + +const PER_INSTALLATION_DEFAULTS_BRANCH = "__DEFAULTS__"; + +/** + * Some prefs are specific to the installation, not the profile. They are + * stored in JSON format in FILE_UPDATE_CONFIG_JSON. + * Not all platforms currently support per-installation prefs, in which case + * we fall back to using profile-specific prefs. + * + * Note: These prefs should always be accessed through UpdateUtils. Do NOT + * attempt to read or write their prefs directly. + * + * Keys in this object should be the name of the pref. The same name will be + * used whether we are writing it to the per-installation or per-profile pref. + * Values in this object should be objects with the following keys: + * type + * Must be one of the Update.PER_INSTALLATION_PREF_TYPE_* values, defined + * above. + * defaultValue + * The default value to use for this pref if no value is set. This must be + * of a type that is compatible with the type value specified. + * migrate + * Optional - defaults to false. A boolean indicating whether an existing + * value in the profile-specific prefs ought to be migrated to an + * installation specific pref. This is useful for prefs like + * app.update.auto that used to be profile-specific prefs. + * Note - Migration currently happens only on the creation of the JSON + * file. If we want to add more prefs that require migration, we + * will probably need to change this. + * observerTopic + * When a config value is changed, an observer will be fired, much like + * the existing preference observers. This specifies the topic of the + * observer that will be fired. + * policyFn + * Optional. If defined, should be a function that returns null or a value + * of the specified type of this pref. If null is returned, this has no + * effect. If another value is returned, it will be used rather than + * reading the pref. This function will only be called if + * Services.policies is defined. Asynchronous functions are not currently + * supported. + */ +UpdateUtils.PER_INSTALLATION_PREFS = { + "app.update.auto": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_BOOL, + defaultValue: true, + migrate: true, + observerTopic: "auto-update-config-change", + policyFn: () => { + if (!Services.policies.isAllowed("app-auto-updates-off")) { + // We aren't allowed to turn off auto-update - it is forced on. + return true; + } + if (!Services.policies.isAllowed("app-auto-updates-on")) { + // We aren't allowed to turn on auto-update - it is forced off. + return false; + } + return null; + }, + }, + "app.update.background.enabled": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_BOOL, + defaultValue: true, + observerTopic: "background-update-config-change", + policyFn: () => { + if (!Services.policies.isAllowed("app-background-update-off")) { + // We aren't allowed to turn off background update - it is forced on. + return true; + } + if (!Services.policies.isAllowed("app-background-update-on")) { + // We aren't allowed to turn on background update - it is forced off. + return false; + } + return null; + }, + }, +}; + +const TYPE_SPECIFIC_PREF_FNS = { + [UpdateUtils.PER_INSTALLATION_PREF_TYPE_BOOL]: { + getProfilePref: Services.prefs.getBoolPref, + setProfilePref: Services.prefs.setBoolPref, + setProfileDefaultPref: (pref, value) => { + let defaults = Services.prefs.getDefaultBranch(""); + defaults.setBoolPref(pref, value); + }, + isValid: value => typeof value == "boolean", + }, + [UpdateUtils.PER_INSTALLATION_PREF_TYPE_ASCII_STRING]: { + getProfilePref: Services.prefs.getCharPref, + setProfilePref: Services.prefs.setCharPref, + setProfileDefaultPref: (pref, value) => { + let defaults = Services.prefs.getDefaultBranch(""); + defaults.setCharPref(pref, value); + }, + isValid: value => typeof value == "string", + }, + [UpdateUtils.PER_INSTALLATION_PREF_TYPE_INT]: { + getProfilePref: Services.prefs.getIntPref, + setProfilePref: Services.prefs.setIntPref, + setProfileDefaultPref: (pref, value) => { + let defaults = Services.prefs.getDefaultBranch(""); + defaults.setIntPref(pref, value); + }, + isValid: value => Number.isInteger(value), + }, +}; + +/** + * Used for serializing reads and writes of the app update json config file so + * the writes don't happen out of order and the last write is the one that + * the sets the value. + */ +var updateConfigIOPromise = Promise.resolve(); + +/** + * Returns a pref name that we will use to keep track of if the passed pref has + * been migrated already, so we don't end up migrating it twice. + */ +function getPrefMigratedPref(prefName) { + return prefName + ".migrated"; +} + +/** + * @return true if prefs need to be migrated from profile-specific prefs to + * installation-specific prefs. + */ +function updateConfigNeedsMigration() { + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + if (pref.migrate) { + let migratedPrefName = getPrefMigratedPref(prefName); + let migrated = Services.prefs.getBoolPref(migratedPrefName, false); + if (!migrated) { + return true; + } + } + } + return false; +} + +function setUpdateConfigMigrationDone() { + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + if (pref.migrate) { + let migratedPrefName = getPrefMigratedPref(prefName); + Services.prefs.setBoolPref(migratedPrefName, true); + } + } +} + +/** + * Deletes the migrated data. + */ +function onMigrationSuccessful() { + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + if (pref.migrate) { + Services.prefs.clearUserPref(prefName); + } + } +} + +function makeMigrationUpdateConfig() { + let config = makeDefaultUpdateConfig(); + + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + if (!pref.migrate) { + continue; + } + let migratedPrefName = getPrefMigratedPref(prefName); + let alreadyMigrated = Services.prefs.getBoolPref(migratedPrefName, false); + if (alreadyMigrated) { + continue; + } + + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + let prefHasValue = true; + let prefValue; + try { + // Without a second argument, this will throw if the pref has no user + // value or default value. + prefValue = prefTypeFns.getProfilePref(prefName); + } catch (e) { + prefHasValue = false; + } + if (prefHasValue) { + setConfigValue(config, prefName, prefValue); + } + } + + return config; +} + +function makeDefaultUpdateConfig() { + let config = {}; + + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + setConfigValue(config, prefName, pref.defaultValue, { + setDefaultOnly: true, + }); + } + + return config; +} + +/** + * Sets the specified value in the config object. + * + * @param config + * The config object for which to set the value + * @param prefName + * The name of the preference to set. + * @param prefValue + * The value to set the preference to. + * @param options + * Optional. An object containing any of the following keys: + * setDefaultOnly + * If set to true, the default value will be set rather than + * user value. If a user value is set for this pref, this will + * have no effect on the pref's effective value. + */ +function setConfigValue(config, prefName, prefValue, options) { + if (!options) { + options = {}; + } + + if (options.setDefaultOnly) { + if (!(PER_INSTALLATION_DEFAULTS_BRANCH in config)) { + config[PER_INSTALLATION_DEFAULTS_BRANCH] = {}; + } + config[PER_INSTALLATION_DEFAULTS_BRANCH][prefName] = prefValue; + } else if (prefValue != readDefaultValue(config, prefName)) { + config[prefName] = prefValue; + } else { + delete config[prefName]; + } +} + +/** + * Reads the specified pref out of the given configuration object. + * If a user value of the pref is set, that will be returned. If only a default + * branch value is set, that will be returned. Otherwise, the default value from + * PER_INSTALLATION_PREFS will be returned. + * + * Values will be validated before being returned. Invalid values are ignored. + * + * @param config + * The configuration object to read. + * @param prefName + * The name of the preference to read. + * @return The value of the preference. + */ +function readEffectiveValue(config, prefName) { + if (!(prefName in UpdateUtils.PER_INSTALLATION_PREFS)) { + throw new Error( + `readEffectiveValue: Unknown per-installation pref '${prefName}'` + ); + } + const pref = UpdateUtils.PER_INSTALLATION_PREFS[prefName]; + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + if (prefName in config) { + if (prefTypeFns.isValid(config[prefName])) { + return config[prefName]; + } + console.error( + `readEffectiveValue: Got invalid value for update config's` + + ` '${prefName}' value: "${config[prefName]}"` + ); + } + return readDefaultValue(config, prefName); +} + +/** + * Reads the default branch pref out of the given configuration object. If one + * is not set, the default value from PER_INSTALLATION_PREFS will be returned. + * + * Values will be validated before being returned. Invalid values are ignored. + * + * @param config + * The configuration object to read. + * @param prefName + * The name of the preference to read. + * @return The value of the preference. + */ +function readDefaultValue(config, prefName) { + if (!(prefName in UpdateUtils.PER_INSTALLATION_PREFS)) { + throw new Error( + `readDefaultValue: Unknown per-installation pref '${prefName}'` + ); + } + const pref = UpdateUtils.PER_INSTALLATION_PREFS[prefName]; + const prefTypeFns = TYPE_SPECIFIC_PREF_FNS[pref.type]; + + if (PER_INSTALLATION_DEFAULTS_BRANCH in config) { + let defaults = config[PER_INSTALLATION_DEFAULTS_BRANCH]; + if (prefName in defaults) { + if (prefTypeFns.isValid(defaults[prefName])) { + return defaults[prefName]; + } + console.error( + `readEffectiveValue: Got invalid default value for update` + + ` config's '${prefName}' value: "${defaults[prefName]}"` + ); + } + } + return pref.defaultValue; +} + +/** + * Reads the update config and, if necessary, performs migration of un-migrated + * values. We don't want to completely give up on update if this file is + * unavailable, so default values will be returned on failure rather than + * throwing an error. + * + * @return An Update Config object. + */ +async function readUpdateConfig() { + try { + let config = await IOUtils.readJSON(UpdateUtils.getConfigFilePath()); + + // We only migrate once. If we read something, the migration has already + // happened so we should make sure it doesn't happen again. + setUpdateConfigMigrationDone(); + + return config; + } catch (e) { + if (DOMException.isInstance(e) && e.name == "NotFoundError") { + if (updateConfigNeedsMigration()) { + const migrationConfig = makeMigrationUpdateConfig(); + setUpdateConfigMigrationDone(); + try { + await writeUpdateConfig(migrationConfig); + onMigrationSuccessful(); + return migrationConfig; + } catch (e) { + console.error("readUpdateConfig: Migration failed: ", e); + } + } + } else { + // We only migrate once. If we got an error other than the file not + // existing, the migration has already happened so we should make sure + // it doesn't happen again. + setUpdateConfigMigrationDone(); + + console.error( + "readUpdateConfig: Unable to read app update configuration file. " + + "Exception: ", + e + ); + } + return makeDefaultUpdateConfig(); + } +} + +/** + * Writes the given configuration to the disk. + * + * @param config + * The configuration object to write. + * @return The configuration object written. + * @throw A DOMException will be thrown on I/O error. + */ +async function writeUpdateConfig(config) { + let path = UpdateUtils.getConfigFilePath(); + await IOUtils.writeJSON(path, config, { tmpPath: `${path}.tmp` }); + return config; +} + +var gUpdateConfigCache; +/** + * Notifies observers if any update config prefs have changed. + * + * @param config + * The most up-to-date config object. + * @return The same config object that was passed in. + */ +function maybeUpdateConfigChanged(config) { + if (!gUpdateConfigCache) { + // We don't want to generate a change notification for every pref on the + // first read of the session. + gUpdateConfigCache = config; + return config; + } + + for (const [prefName, pref] of Object.entries( + UpdateUtils.PER_INSTALLATION_PREFS + )) { + let newPrefValue = readEffectiveValue(config, prefName); + let oldPrefValue = readEffectiveValue(gUpdateConfigCache, prefName); + if (newPrefValue != oldPrefValue) { + Services.obs.notifyObservers( + null, + pref.observerTopic, + newPrefValue.toString() + ); + } + } + + gUpdateConfigCache = config; + return config; +} + +/** + * Note that this function sets up observers only, it does not do any I/O. + */ +UpdateUtils.initPerInstallPrefs(); + +/* Get the distribution pref values, from defaults only */ +function getDistributionPrefValue(aPrefName) { + let value = Services.prefs + .getDefaultBranch(null) + .getCharPref(aPrefName, "default"); + if (!value) { + value = "default"; + } + return value; +} + +function getSystemCapabilities() { + return "ISET:" + lazy.gInstructionSet + ",MEM:" + getMemoryMB(); +} + +/** + * Gets the RAM size in megabytes. This will round the value because sysinfo + * doesn't always provide RAM in multiples of 1024. + */ +function getMemoryMB() { + let memoryMB = "unknown"; + try { + memoryMB = Services.sysinfo.getProperty("memsize"); + if (memoryMB) { + memoryMB = Math.round(memoryMB / 1024 / 1024); + } + } catch (e) { + console.error("Error getting system info memsize property. Exception: ", e); + } + return memoryMB; +} + +/** + * Gets the supported CPU instruction set. + */ +XPCOMUtils.defineLazyGetter(lazy, "gInstructionSet", function aus_gIS() { + const CPU_EXTENSIONS = [ + "hasSSE4_2", + "hasSSE4_1", + "hasSSE4A", + "hasSSSE3", + "hasSSE3", + "hasSSE2", + "hasSSE", + "hasMMX", + "hasNEON", + "hasARMv7", + "hasARMv6", + ]; + for (let ext of CPU_EXTENSIONS) { + if (Services.sysinfo.getProperty(ext)) { + return ext.substring(3); + } + } + + return "unknown"; +}); + +/* Windows only getter that returns the processor architecture. */ +XPCOMUtils.defineLazyGetter(lazy, "gWinCPUArch", function aus_gWinCPUArch() { + // Get processor architecture + let arch = "unknown"; + + const WORD = lazy.ctypes.uint16_t; + const DWORD = lazy.ctypes.uint32_t; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx + const SYSTEM_INFO = new lazy.ctypes.StructType("SYSTEM_INFO", [ + { wProcessorArchitecture: WORD }, + { wReserved: WORD }, + { dwPageSize: DWORD }, + { lpMinimumApplicationAddress: lazy.ctypes.voidptr_t }, + { lpMaximumApplicationAddress: lazy.ctypes.voidptr_t }, + { dwActiveProcessorMask: DWORD.ptr }, + { dwNumberOfProcessors: DWORD }, + { dwProcessorType: DWORD }, + { dwAllocationGranularity: DWORD }, + { wProcessorLevel: WORD }, + { wProcessorRevision: WORD }, + ]); + + let kernel32 = false; + try { + kernel32 = lazy.ctypes.open("Kernel32"); + } catch (e) { + console.error("Unable to open kernel32! Exception: ", e); + } + + if (kernel32) { + try { + let GetNativeSystemInfo = kernel32.declare( + "GetNativeSystemInfo", + lazy.ctypes.winapi_abi, + lazy.ctypes.void_t, + SYSTEM_INFO.ptr + ); + let winSystemInfo = SYSTEM_INFO(); + // Default to unknown + winSystemInfo.wProcessorArchitecture = 0xffff; + + GetNativeSystemInfo(winSystemInfo.address()); + switch (winSystemInfo.wProcessorArchitecture) { + case 12: + arch = "aarch64"; + break; + case 9: + arch = "x64"; + break; + case 6: + arch = "IA64"; + break; + case 0: + arch = "x86"; + break; + } + } catch (e) { + console.error("Error getting processor architecture. Exception: ", e); + } finally { + kernel32.close(); + } + } + + return arch; +}); + +XPCOMUtils.defineLazyGetter(UpdateUtils, "ABI", function () { + let abi = null; + try { + abi = Services.appinfo.XPCOMABI; + } catch (e) { + console.error("XPCOM ABI unknown"); + } + + if (AppConstants.platform == "win") { + // Windows build should report the CPU architecture that it's running on. + abi += "-" + lazy.gWinCPUArch; + } + + if (AppConstants.ASAN) { + // Allow ASan builds to receive their own updates + abi += "-asan"; + } + + return abi; +}); + +XPCOMUtils.defineLazyGetter(UpdateUtils, "OSVersion", function () { + let osVersion; + try { + osVersion = + Services.sysinfo.getProperty("name") + + " " + + Services.sysinfo.getProperty("version"); + } catch (e) { + console.error("OS Version unknown."); + } + + if (osVersion) { + if (AppConstants.platform == "win") { + // Add service pack and build number + try { + const { servicePackMajor, servicePackMinor, buildNumber } = + lazy.WindowsVersionInfo.get(); + osVersion += `.${servicePackMajor}.${servicePackMinor}.${buildNumber}`; + } catch (err) { + console.error("Unable to retrieve windows version information: ", err); + osVersion += ".unknown"; + } + + // add UBR if on Windows 10 + if ( + Services.vc.compare(Services.sysinfo.getProperty("version"), "10") >= 0 + ) { + const WINDOWS_UBR_KEY_PATH = + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + let ubr = lazy.WindowsRegistry.readRegKey( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + WINDOWS_UBR_KEY_PATH, + "UBR", + Ci.nsIWindowsRegKey.WOW64_64 + ); + if (ubr !== undefined) { + osVersion += `.${ubr}`; + } else { + osVersion += ".unknown"; + } + } + + // Add processor architecture + osVersion += " (" + lazy.gWinCPUArch + ")"; + } + + try { + osVersion += + " (" + Services.sysinfo.getProperty("secondaryLibrary") + ")"; + } catch (e) { + // Not all platforms have a secondary widget library, so an error is nothing to worry about. + } + osVersion = encodeURIComponent(osVersion); + } + return osVersion; +}); |