diff options
Diffstat (limited to 'toolkit/modules/UpdateUtils.jsm')
-rw-r--r-- | toolkit/modules/UpdateUtils.jsm | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/toolkit/modules/UpdateUtils.jsm b/toolkit/modules/UpdateUtils.jsm new file mode 100644 index 0000000000..d2b695b474 --- /dev/null +++ b/toolkit/modules/UpdateUtils.jsm @@ -0,0 +1,600 @@ +/* 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 = ["UpdateUtils"]; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { FileUtils } = ChromeUtils.import( + "resource://gre/modules/FileUtils.jsm" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); +ChromeUtils.defineModuleGetter( + this, + "WindowsVersionInfo", + "resource://gre/modules/components-utils/WindowsVersionInfo.jsm" +); +XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); /* globals fetch */ + +ChromeUtils.defineModuleGetter( + this, + "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm" +); + +// 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"; +// Do not use the PREF_APP_UPDATE_AUTO preference directly! +// Call getAppUpdateAutoEnabled or setAppUpdateAutoEnabled instead. +const PREF_APP_UPDATE_AUTO = "app.update.auto"; +const PREF_APP_UPDATE_AUTO_MIGRATED = "app.update.auto.migrated"; +// The setting name in the FILE_UPDATE_CONFIG_JSON file for whether the +// Application Update Service automatically downloads and installs updates. +const CONFIG_APP_UPDATE_AUTO = "app.update.auto"; +// The default value for the CONFIG_APP_UPDATE_AUTO setting and the +// PREF_APP_UPDATE_AUTO preference. +const DEFAULT_APP_UPDATE_AUTO = true; + +var UpdateUtils = { + _locale: 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) { + Cu.reportError(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()); + } + } + + Cu.reportError( + FILE_UPDATE_LOCALE + + " file doesn't exist in either the " + + "application or GRE directories" + ); + + return (this._locale = null); + }, + + /** + * 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. + */ + getAppUpdateAutoEnabled() { + if (Services.policies) { + if (!Services.policies.isAllowed("app-auto-updates-off")) { + // We aren't allowed to turn off auto-update - it is forced on. + return Promise.resolve(true); + } + if (!Services.policies.isAllowed("app-auto-updates-on")) { + // We aren't allowed to turn on auto-update - it is forced off. + return Promise.resolve(false); + } + } + if (AppConstants.platform != "win") { + // On platforms other than Windows the setting is stored in a preference. + let prefValue = Services.prefs.getBoolPref( + PREF_APP_UPDATE_AUTO, + DEFAULT_APP_UPDATE_AUTO + ); + return Promise.resolve(prefValue); + } + // Justification for the empty catch statement below: + // All promises returned by (get|set)AutoUpdateIsEnabled are part of a + // single promise chain in order to serialize disk operations. We don't want + // the entire promise chain to reject when one operation fails. + // + // There is only one situation when a promise in this chain should ever + // reject, which is when writing fails and the error is logged and + // re-thrown. All other possible exceptions are wrapped in try blocks, which + // also log any exception that may occur. + let readPromise = updateAutoIOPromise + .catch(() => {}) + .then(async () => { + try { + let configValue = await readUpdateAutoConfig(); + // If we read a value out of this file, don't later perform migration. + // If the file is deleted, we don't want some stale pref getting + // written to it just because a different profile performed migration. + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO_MIGRATED, true); + return configValue; + } catch (e) { + // Not being able to read from the app update configuration file is not + // a serious issue so use logStringMessage to avoid concern from users. + Services.console.logStringMessage( + "UpdateUtils.getAppUpdateAutoEnabled - Unable to read app update " + + "configuration file. Exception: " + + e + ); + let valueMigrated = Services.prefs.getBoolPref( + PREF_APP_UPDATE_AUTO_MIGRATED, + false + ); + if (!valueMigrated) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO_MIGRATED, true); + let prefValue = Services.prefs.getBoolPref( + PREF_APP_UPDATE_AUTO, + DEFAULT_APP_UPDATE_AUTO + ); + try { + let writtenValue = await writeUpdateAutoConfig(prefValue); + Services.prefs.clearUserPref(PREF_APP_UPDATE_AUTO); + return writtenValue; + } catch (e) { + Cu.reportError( + "UpdateUtils.getAppUpdateAutoEnabled - Migration " + + "failed. Exception: " + + e + ); + } + } + } + // Fallthrough for if the value could not be read or migrated. + return DEFAULT_APP_UPDATE_AUTO; + }) + .then(maybeUpdateAutoConfigChanged); + updateAutoIOPromise = readPromise; + return readPromise; + }, + + /** + * 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. + */ + setAppUpdateAutoEnabled(enabledValue) { + if (this.appUpdateAutoSettingIsLocked()) { + return Promise.reject( + "setAppUpdateAutoEnabled: Unable to change value of setting because " + + "it is locked by policy" + ); + } + if (AppConstants.platform != "win") { + // Only in Windows do we store the update config in the update directory + let prefValue = !!enabledValue; + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, prefValue); + // Rather than call maybeUpdateAutoConfigChanged, a pref observer has + // been connected to PREF_APP_UPDATE_AUTO. This allows us to catch direct + // changes to the pref (which Firefox shouldn't be doing, but the user + // might do in about:config). + return Promise.resolve(prefValue); + } + // Justification for the empty catch statement below: + // All promises returned by (get|set)AutoUpdateIsEnabled are part of a + // single promise chain in order to serialize disk operations. We don't want + // the entire promise chain to reject when one operation fails. + // + // There is only one situation when a promise in this chain should ever + // reject, which is when writing fails and the error is logged and + // re-thrown. All other possible exceptions are wrapped in try blocks, which + // also log any exception that may occur. + let writePromise = updateAutoIOPromise + .catch(() => {}) + .then(async () => { + try { + return await writeUpdateAutoConfig(enabledValue); + } catch (e) { + Cu.reportError( + "UpdateUtils.setAppUpdateAutoEnabled - App update " + + "configuration file write failed. Exception: " + + e + ); + // Rethrow the error so the caller knows that writing the value in the + // app update config file failed. + throw e; + } + }) + .then(maybeUpdateAutoConfigChanged); + updateAutoIOPromise = writePromise; + return writePromise; + }, + + /** + * 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 ( + Services.policies && + (!Services.policies.isAllowed("app-auto-updates-off") || + !Services.policies.isAllowed("app-auto-updates-on")) + ); + }, +}; + +// 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 updateAutoIOPromise = Promise.resolve(); +var updateAutoSettingCachedVal = null; + +async function readUpdateAutoConfig() { + let configFile = FileUtils.getDir("UpdRootD", [], true); + configFile.append(FILE_UPDATE_CONFIG_JSON); + let binaryData = await OS.File.read(configFile.path); + let jsonData = new TextDecoder().decode(binaryData); + let configData = JSON.parse(jsonData); + return !!configData[CONFIG_APP_UPDATE_AUTO]; +} + +async function writeUpdateAutoConfig(enabledValue) { + let enabledBoolValue = !!enabledValue; + let configFile = FileUtils.getDir("UpdRootD", [], true); + configFile.append(FILE_UPDATE_CONFIG_JSON); + let configObject = { [CONFIG_APP_UPDATE_AUTO]: enabledBoolValue }; + await OS.File.writeAtomic(configFile.path, JSON.stringify(configObject)); + return enabledBoolValue; +} + +// Notifies observers if the value of app.update.auto has changed and returns +// the value for app.update.auto. +function maybeUpdateAutoConfigChanged(newValue) { + if (newValue !== updateAutoSettingCachedVal) { + updateAutoSettingCachedVal = newValue; + Services.obs.notifyObservers( + null, + "auto-update-config-change", + newValue.toString() + ); + } + return newValue; +} +// On non-Windows platforms, the Update Auto Config is still stored as a pref. +// On those platforms, the best way to notify observers of this setting is +// just to propagate it from a pref observer +if (AppConstants.platform != "win") { + Services.prefs.addObserver( + PREF_APP_UPDATE_AUTO, + async (subject, topic, data) => { + let value = await UpdateUtils.getAppUpdateAutoEnabled(); + maybeUpdateAutoConfigChanged(value); + } + ); +} + +/* 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:" + 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) { + Cu.reportError( + "Error getting system info memsize property. Exception: " + e + ); + } + return memoryMB; +} + +/** + * Gets the supported CPU instruction set. + */ +XPCOMUtils.defineLazyGetter(this, "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(this, "gWinCPUArch", function aus_gWinCPUArch() { + // Get processor architecture + let arch = "unknown"; + + const WORD = ctypes.uint16_t; + const DWORD = 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 ctypes.StructType("SYSTEM_INFO", [ + { wProcessorArchitecture: WORD }, + { wReserved: WORD }, + { dwPageSize: DWORD }, + { lpMinimumApplicationAddress: ctypes.voidptr_t }, + { lpMaximumApplicationAddress: ctypes.voidptr_t }, + { dwActiveProcessorMask: DWORD.ptr }, + { dwNumberOfProcessors: DWORD }, + { dwProcessorType: DWORD }, + { dwAllocationGranularity: DWORD }, + { wProcessorLevel: WORD }, + { wProcessorRevision: WORD }, + ]); + + let kernel32 = false; + try { + kernel32 = ctypes.open("Kernel32"); + } catch (e) { + Cu.reportError("Unable to open kernel32! Exception: " + e); + } + + if (kernel32) { + try { + let GetNativeSystemInfo = kernel32.declare( + "GetNativeSystemInfo", + ctypes.winapi_abi, + 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) { + Cu.reportError("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) { + Cu.reportError("XPCOM ABI unknown"); + } + + if (AppConstants.platform == "win") { + // Windows build should report the CPU architecture that it's running on. + abi += "-" + 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) { + Cu.reportError("OS Version unknown."); + } + + if (osVersion) { + if (AppConstants.platform == "win") { + // Add service pack and build number + try { + const { + servicePackMajor, + servicePackMinor, + buildNumber, + } = WindowsVersionInfo.get(); + osVersion += `.${servicePackMajor}.${servicePackMinor}.${buildNumber}`; + } catch (err) { + Cu.reportError( + "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 = 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 += " (" + 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; +}); |