michael@0: #filter substitution michael@0: michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/FileUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/AddonManager.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: #ifdef BUILD_CTYPES michael@0: #ifdef XP_WIN michael@0: Components.utils.import("resource://gre/modules/ctypes.jsm"); michael@0: #endif michael@0: #endif michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"); michael@0: const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1"; michael@0: michael@0: const PREF_APP_UPDATE_ALTWINDOWTYPE = "app.update.altwindowtype"; michael@0: const PREF_APP_UPDATE_AUTO = "app.update.auto"; michael@0: const PREF_APP_UPDATE_BACKGROUND_INTERVAL = "app.update.download.backgroundInterval"; michael@0: const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; michael@0: const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; michael@0: const PREF_APP_UPDATE_CERTS_BRANCH = "app.update.certs."; michael@0: const PREF_APP_UPDATE_CERT_CHECKATTRS = "app.update.cert.checkAttributes"; michael@0: const PREF_APP_UPDATE_CERT_ERRORS = "app.update.cert.errors"; michael@0: const PREF_APP_UPDATE_CERT_MAXERRORS = "app.update.cert.maxErrors"; michael@0: const PREF_APP_UPDATE_CERT_REQUIREBUILTIN = "app.update.cert.requireBuiltIn"; michael@0: const PREF_APP_UPDATE_CUSTOM = "app.update.custom"; michael@0: const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; michael@0: const PREF_APP_UPDATE_METRO_ENABLED = "app.update.metro.enabled"; michael@0: const PREF_APP_UPDATE_IDLETIME = "app.update.idletime"; michael@0: const PREF_APP_UPDATE_INCOMPATIBLE_MODE = "app.update.incompatible.mode"; michael@0: const PREF_APP_UPDATE_INTERVAL = "app.update.interval"; michael@0: const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer"; michael@0: const PREF_APP_UPDATE_LOG = "app.update.log"; michael@0: const PREF_APP_UPDATE_MODE = "app.update.mode"; michael@0: const PREF_APP_UPDATE_NEVER_BRANCH = "app.update.never."; michael@0: const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; michael@0: const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate"; michael@0: const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; michael@0: const PREF_APP_UPDATE_SHOW_INSTALLED_UI = "app.update.showInstalledUI"; michael@0: const PREF_APP_UPDATE_SILENT = "app.update.silent"; michael@0: const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; michael@0: const PREF_APP_UPDATE_URL = "app.update.url"; michael@0: const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; michael@0: const PREF_APP_UPDATE_URL_OVERRIDE = "app.update.url.override"; michael@0: const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; michael@0: const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors"; michael@0: const PREF_APP_UPDATE_SERVICE_MAX_ERRORS = "app.update.service.maxErrors"; michael@0: const PREF_APP_UPDATE_SOCKET_ERRORS = "app.update.socket.maxErrors"; michael@0: const PREF_APP_UPDATE_RETRY_TIMEOUT = "app.update.socket.retryTimeout"; michael@0: michael@0: const PREF_APP_DISTRIBUTION = "distribution.id"; michael@0: const PREF_APP_DISTRIBUTION_VERSION = "distribution.version"; michael@0: michael@0: const PREF_APP_B2G_VERSION = "b2g.version"; michael@0: michael@0: const PREF_EM_HOTFIX_ID = "extensions.hotfix.id"; michael@0: michael@0: const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul"; michael@0: const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul"; michael@0: const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; michael@0: const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties"; michael@0: const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update"; michael@0: michael@0: const CATEGORY_UPDATE_TIMER = "update-timer"; michael@0: michael@0: const KEY_GRED = "GreD"; michael@0: const KEY_UPDROOT = "UpdRootD"; michael@0: const KEY_EXECUTABLE = "XREExeF"; michael@0: michael@0: #ifdef TOR_BROWSER_VERSION michael@0: #expand const TOR_BROWSER_VERSION = __TOR_BROWSER_VERSION__; michael@0: #endif michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: #define USE_UPDATE_ARCHIVE_DIR michael@0: #endif michael@0: michael@0: #ifdef USE_UPDATE_ARCHIVE_DIR michael@0: const KEY_UPDATE_ARCHIVE_DIR = "UpdArchD" michael@0: #endif michael@0: michael@0: #ifdef XP_WIN michael@0: #define CHECK_CAN_USE_SERVICE michael@0: #elifdef MOZ_WIDGET_GONK michael@0: // In Gonk, the updater will remount the /system partition to move staged files michael@0: // into place, so we skip the test here to keep things isolated. michael@0: #define CHECK_CAN_USE_SERVICE michael@0: #endif michael@0: michael@0: const DIR_UPDATES = "updates"; michael@0: #ifdef XP_MACOSX michael@0: const UPDATED_DIR = "Updated.app"; michael@0: #else michael@0: const UPDATED_DIR = "updated"; michael@0: #endif michael@0: const FILE_UPDATE_STATUS = "update.status"; michael@0: const FILE_UPDATE_VERSION = "update.version"; michael@0: #ifdef MOZ_WIDGET_ANDROID michael@0: const FILE_UPDATE_ARCHIVE = "update.apk"; michael@0: #else michael@0: const FILE_UPDATE_ARCHIVE = "update.mar"; michael@0: #endif michael@0: const FILE_UPDATE_LINK = "update.link"; michael@0: const FILE_UPDATE_LOG = "update.log"; michael@0: const FILE_UPDATES_DB = "updates.xml"; michael@0: const FILE_UPDATE_ACTIVE = "active-update.xml"; michael@0: const FILE_PERMS_TEST = "update.test"; michael@0: const FILE_LAST_LOG = "last-update.log"; michael@0: const FILE_BACKUP_LOG = "backup-update.log"; michael@0: const FILE_UPDATE_LOCALE = "update.locale"; michael@0: michael@0: const STATE_NONE = "null"; michael@0: const STATE_DOWNLOADING = "downloading"; michael@0: const STATE_PENDING = "pending"; michael@0: const STATE_PENDING_SVC = "pending-service"; michael@0: const STATE_APPLYING = "applying"; michael@0: const STATE_APPLIED = "applied"; michael@0: const STATE_APPLIED_OS = "applied-os"; michael@0: const STATE_APPLIED_SVC = "applied-service"; michael@0: const STATE_SUCCEEDED = "succeeded"; michael@0: const STATE_DOWNLOAD_FAILED = "download-failed"; michael@0: const STATE_FAILED = "failed"; michael@0: michael@0: // From updater/errors.h: michael@0: const WRITE_ERROR = 7; michael@0: // const UNEXPECTED_ERROR = 8; // Replaced with errors 38-42 michael@0: const ELEVATION_CANCELED = 9; michael@0: michael@0: // Windows service specific errors michael@0: const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24; michael@0: const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25; michael@0: const SERVICE_UPDATER_SIGN_ERROR = 26; michael@0: const SERVICE_UPDATER_COMPARE_ERROR = 27; michael@0: const SERVICE_UPDATER_IDENTITY_ERROR = 28; michael@0: const SERVICE_STILL_APPLYING_ON_SUCCESS = 29; michael@0: const SERVICE_STILL_APPLYING_ON_FAILURE = 30; michael@0: const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31; michael@0: const SERVICE_COULD_NOT_LOCK_UPDATER = 32; michael@0: const SERVICE_INSTALLDIR_ERROR = 33; michael@0: const SERVICE_COULD_NOT_COPY_UPDATER = 49; michael@0: michael@0: const WRITE_ERROR_ACCESS_DENIED = 35; michael@0: // const WRITE_ERROR_SHARING_VIOLATION = 36; // Replaced with errors 46-48 michael@0: const WRITE_ERROR_CALLBACK_APP = 37; michael@0: const INVALID_UPDATER_STATUS_CODE = 38; michael@0: const UNEXPECTED_BZIP_ERROR = 39; michael@0: const UNEXPECTED_MAR_ERROR = 40; michael@0: const UNEXPECTED_BSPATCH_ERROR = 41; michael@0: const UNEXPECTED_FILE_OPERATION_ERROR = 42; michael@0: const FILESYSTEM_MOUNT_READWRITE_ERROR = 43; michael@0: const FOTA_GENERAL_ERROR = 44; michael@0: const FOTA_UNKNOWN_ERROR = 45; michael@0: const WRITE_ERROR_SHARING_VIOLATION_SIGNALED = 46; michael@0: const WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID = 47; michael@0: const WRITE_ERROR_SHARING_VIOLATION_NOPID = 48; michael@0: const FOTA_FILE_OPERATION_ERROR = 49; michael@0: const FOTA_RECOVERY_ERROR = 50; michael@0: michael@0: const CERT_ATTR_CHECK_FAILED_NO_UPDATE = 100; michael@0: const CERT_ATTR_CHECK_FAILED_HAS_UPDATE = 101; michael@0: const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; michael@0: const NETWORK_ERROR_OFFLINE = 111; michael@0: const FILE_ERROR_TOO_BIG = 112; michael@0: michael@0: // Error codes should be < 1000. Errors above 1000 represent http status codes michael@0: const HTTP_ERROR_OFFSET = 1000; michael@0: michael@0: const DOWNLOAD_CHUNK_SIZE = 300000; // bytes michael@0: const DOWNLOAD_BACKGROUND_INTERVAL = 600; // seconds michael@0: const DOWNLOAD_FOREGROUND_INTERVAL = 0; michael@0: michael@0: const UPDATE_WINDOW_NAME = "Update:Wizard"; michael@0: michael@0: // The number of consecutive failures when updating using the service before michael@0: // setting the app.update.service.enabled preference to false. michael@0: const DEFAULT_SERVICE_MAX_ERRORS = 10; michael@0: michael@0: // The number of consecutive socket errors to allow before falling back to michael@0: // downloading a different MAR file or failing if already downloading the full. michael@0: const DEFAULT_SOCKET_MAX_ERRORS = 10; michael@0: michael@0: // The number of milliseconds to wait before retrying a connection error. michael@0: const DEFAULT_UPDATE_RETRY_TIMEOUT = 2000; michael@0: michael@0: // A background download is in progress (no notification) michael@0: const PING_BGUC_IS_DOWNLOADING = 0; michael@0: // An update is staged (no notification) michael@0: const PING_BGUC_IS_STAGED = 1; michael@0: // Invalid url for app.update.url default preference (no notification) michael@0: const PING_BGUC_INVALID_DEFAULT_URL = 2; michael@0: // Invalid url for app.update.url user preference (no notification) michael@0: const PING_BGUC_INVALID_CUSTOM_URL = 3; michael@0: // Invalid url for app.update.url.override user preference (no notification) michael@0: const PING_BGUC_INVALID_OVERRIDE_URL = 4; michael@0: // Unable to check for updates per gCanCheckForUpdates and hasUpdateMutex() michael@0: // (no notification) michael@0: const PING_BGUC_UNABLE_TO_CHECK = 5; michael@0: // Already has an active update in progress (no notification) michael@0: const PING_BGUC_HAS_ACTIVEUPDATE = 6; michael@0: // Background checks disabled by preference (no notification) michael@0: const PING_BGUC_PREF_DISABLED = 7; michael@0: // Background checks disabled for the current session (no notification) michael@0: const PING_BGUC_DISABLED_FOR_SESSION = 8; michael@0: // Background checks disabled in Metro (no notification) michael@0: const PING_BGUC_METRO_DISABLED = 9; michael@0: // Unable to perform a background check while offline (no notification) michael@0: const PING_BGUC_OFFLINE = 10; michael@0: // No update found certificate check failed and threshold reached michael@0: // (possible mitm attack notification) michael@0: const PING_BGUC_CERT_ATTR_NO_UPDATE_NOTIFY = 11; michael@0: // No update found certificate check failed and threshold not reached michael@0: // (no notification) michael@0: const PING_BGUC_CERT_ATTR_NO_UPDATE_SILENT = 12; michael@0: // Update found certificate check failed and threshold reached michael@0: // (possible mitm attack notification) michael@0: const PING_BGUC_CERT_ATTR_WITH_UPDATE_NOTIFY = 13; michael@0: // Update found certificate check failed and threshold not reached michael@0: // (no notification) michael@0: const PING_BGUC_CERT_ATTR_WITH_UPDATE_SILENT = 14; michael@0: // General update check failure and threshold reached michael@0: // (check failure notification) michael@0: const PING_BGUC_GENERAL_ERROR_NOTIFY = 15; michael@0: // General update check failure and threshold not reached michael@0: // (no notification) michael@0: const PING_BGUC_GENERAL_ERROR_SILENT = 16; michael@0: // No update found (no notification) michael@0: const PING_BGUC_NO_UPDATE_FOUND = 17; michael@0: // No compatible update found though there were updates (no notification) michael@0: const PING_BGUC_NO_COMPAT_UPDATE_FOUND = 18; michael@0: // Update found for a previous version (no notification) michael@0: const PING_BGUC_UPDATE_PREVIOUS_VERSION = 19; michael@0: // Update found for a version with the never preference set (no notification) michael@0: const PING_BGUC_UPDATE_NEVER_PREF = 20; michael@0: // Update found without a type attribute (no notification) michael@0: const PING_BGUC_UPDATE_INVALID_TYPE = 21; michael@0: // The system is no longer supported (system unsupported notification) michael@0: const PING_BGUC_UNSUPPORTED = 22; michael@0: // Unable to apply updates (manual install to update notification) michael@0: const PING_BGUC_UNABLE_TO_APPLY = 23; michael@0: // Showing prompt due to the update.xml specifying showPrompt michael@0: // (update notification) michael@0: const PING_BGUC_SHOWPROMPT_SNIPPET = 24; michael@0: // Showing prompt due to preference (update notification) michael@0: const PING_BGUC_SHOWPROMPT_PREF = 25; michael@0: // Incompatible add-on check disabled by preference (background download) michael@0: const PING_BGUC_ADDON_PREF_DISABLED = 26; michael@0: // Incompatible add-on not checked not performed due to same update version and michael@0: // app version (background download) michael@0: const PING_BGUC_ADDON_SAME_APP_VER = 27; michael@0: // No incompatible add-ons found during incompatible check (background download) michael@0: const PING_BGUC_CHECK_NO_INCOMPAT = 28; michael@0: // Incompatible add-ons found and all of them have updates (background download) michael@0: const PING_BGUC_ADDON_UPDATES_FOR_INCOMPAT = 29; michael@0: // Incompatible add-ons found (update notification) michael@0: const PING_BGUC_ADDON_HAVE_INCOMPAT = 30; michael@0: michael@0: var gLocale = null; michael@0: var gUpdateMutexHandle = null; michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: var gSDCardMountLock = null; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() { michael@0: return Services.env.get("EXTERNAL_STORAGE"); michael@0: }); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", michael@0: "resource://gre/modules/UpdateChannel.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() { michael@0: return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() { michael@0: return Services.strings.createBundle(URI_UPDATES_PROPERTIES); michael@0: }); michael@0: michael@0: // shared code for suppressing bad cert dialogs michael@0: XPCOMUtils.defineLazyGetter(this, "gCertUtils", function aus_gCertUtils() { michael@0: let temp = { }; michael@0: Cu.import("resource://gre/modules/CertUtils.jsm", temp); michael@0: return temp; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gABI", function aus_gABI() { michael@0: let abi = null; michael@0: try { michael@0: abi = Services.appinfo.XPCOMABI; michael@0: } michael@0: catch (e) { michael@0: LOG("gABI - XPCOM ABI unknown: updates are not possible."); michael@0: } michael@0: #ifdef XP_MACOSX michael@0: // Mac universal build should report a different ABI than either macppc michael@0: // or mactel. michael@0: let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"]. michael@0: getService(Ci.nsIMacUtils); michael@0: michael@0: if (macutils.isUniversalBinary) michael@0: abi += "-u-" + macutils.architecturesInBinary; michael@0: #ifdef MOZ_SHARK michael@0: // Disambiguate optimised and shark nightlies michael@0: abi += "-shark" michael@0: #endif michael@0: #endif michael@0: return abi; michael@0: }); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: XPCOMUtils.defineLazyGetter(this, "gProductModel", function aus_gProductModel() { michael@0: Cu.import("resource://gre/modules/systemlibs.js"); michael@0: return libcutils.property_get("ro.product.model"); michael@0: }); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() { michael@0: let osVersion; michael@0: let sysInfo = Cc["@mozilla.org/system-info;1"]. michael@0: getService(Ci.nsIPropertyBag2); michael@0: try { michael@0: osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version"); michael@0: } michael@0: catch (e) { michael@0: LOG("gOSVersion - OS Version unknown: updates are not possible."); michael@0: } michael@0: michael@0: if (osVersion) { michael@0: #ifdef BUILD_CTYPES michael@0: #ifdef XP_WIN michael@0: const BYTE = ctypes.uint8_t; michael@0: const WORD = ctypes.uint16_t; michael@0: const DWORD = ctypes.uint32_t; michael@0: const WCHAR = ctypes.jschar; michael@0: const BOOL = ctypes.int; michael@0: michael@0: // This structure is described at: michael@0: // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx michael@0: const SZCSDVERSIONLENGTH = 128; michael@0: const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', michael@0: [ michael@0: {dwOSVersionInfoSize: DWORD}, michael@0: {dwMajorVersion: DWORD}, michael@0: {dwMinorVersion: DWORD}, michael@0: {dwBuildNumber: DWORD}, michael@0: {dwPlatformId: DWORD}, michael@0: {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, michael@0: {wServicePackMajor: WORD}, michael@0: {wServicePackMinor: WORD}, michael@0: {wSuiteMask: WORD}, michael@0: {wProductType: BYTE}, michael@0: {wReserved: BYTE} michael@0: ]); michael@0: michael@0: // This structure is described at: michael@0: // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx michael@0: const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO', michael@0: [ michael@0: {wProcessorArchitecture: WORD}, michael@0: {wReserved: WORD}, michael@0: {dwPageSize: DWORD}, michael@0: {lpMinimumApplicationAddress: ctypes.voidptr_t}, michael@0: {lpMaximumApplicationAddress: ctypes.voidptr_t}, michael@0: {dwActiveProcessorMask: DWORD.ptr}, michael@0: {dwNumberOfProcessors: DWORD}, michael@0: {dwProcessorType: DWORD}, michael@0: {dwAllocationGranularity: DWORD}, michael@0: {wProcessorLevel: WORD}, michael@0: {wProcessorRevision: WORD} michael@0: ]); michael@0: michael@0: let kernel32 = false; michael@0: try { michael@0: kernel32 = ctypes.open("Kernel32"); michael@0: } catch (e) { michael@0: LOG("gOSVersion - Unable to open kernel32! " + e); michael@0: osVersion += ".unknown (unknown)"; michael@0: } michael@0: michael@0: if(kernel32) { michael@0: try { michael@0: // Get Service pack info michael@0: try { michael@0: let GetVersionEx = kernel32.declare("GetVersionExW", michael@0: ctypes.default_abi, michael@0: BOOL, michael@0: OSVERSIONINFOEXW.ptr); michael@0: let winVer = OSVERSIONINFOEXW(); michael@0: winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; michael@0: michael@0: if(0 !== GetVersionEx(winVer.address())) { michael@0: osVersion += "." + winVer.wServicePackMajor michael@0: + "." + winVer.wServicePackMinor; michael@0: } else { michael@0: LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)"); michael@0: osVersion += ".unknown"; michael@0: } michael@0: } catch (e) { michael@0: LOG("gOSVersion - error getting service pack information. Exception: " + e); michael@0: osVersion += ".unknown"; michael@0: } michael@0: michael@0: // Get processor architecture michael@0: let arch = "unknown"; michael@0: try { michael@0: let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo", michael@0: ctypes.default_abi, michael@0: ctypes.void_t, michael@0: SYSTEM_INFO.ptr); michael@0: let sysInfo = SYSTEM_INFO(); michael@0: // Default to unknown michael@0: sysInfo.wProcessorArchitecture = 0xffff; michael@0: michael@0: GetNativeSystemInfo(sysInfo.address()); michael@0: switch(sysInfo.wProcessorArchitecture) { michael@0: case 9: michael@0: arch = "x64"; michael@0: break; michael@0: case 6: michael@0: arch = "IA64"; michael@0: break; michael@0: case 0: michael@0: arch = "x86"; michael@0: break; michael@0: } michael@0: } catch (e) { michael@0: LOG("gOSVersion - error getting processor architecture. Exception: " + e); michael@0: } finally { michael@0: osVersion += " (" + arch + ")"; michael@0: } michael@0: } finally { michael@0: kernel32.close(); michael@0: } michael@0: } michael@0: #endif michael@0: #endif michael@0: michael@0: try { michael@0: osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")"; michael@0: } michael@0: catch (e) { michael@0: // Not all platforms have a secondary widget library, so an error is nothing to worry about. michael@0: } michael@0: osVersion = encodeURIComponent(osVersion); michael@0: } michael@0: return osVersion; michael@0: }); michael@0: michael@0: /** michael@0: * Tests to make sure that we can write to a given directory. michael@0: * michael@0: * @param updateTestFile a test file in the directory that needs to be tested. michael@0: * @param createDirectory whether a test directory should be created. michael@0: * @throws if we don't have right access to the directory. michael@0: */ michael@0: function testWriteAccess(updateTestFile, createDirectory) { michael@0: const NORMAL_FILE_TYPE = Ci.nsILocalFile.NORMAL_FILE_TYPE; michael@0: const DIRECTORY_TYPE = Ci.nsILocalFile.DIRECTORY_TYPE; michael@0: if (updateTestFile.exists()) michael@0: updateTestFile.remove(false); michael@0: updateTestFile.create(createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE, michael@0: createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE); michael@0: updateTestFile.remove(false); michael@0: } michael@0: michael@0: #ifdef XP_WIN michael@0: michael@0: /** michael@0: * Closes a Win32 handle michael@0: * michael@0: * @param handle The handle to close michael@0: */ michael@0: function closeHandle(handle) { michael@0: var lib = ctypes.open("kernel32.dll"); michael@0: var CloseHandle = lib.declare("CloseHandle", michael@0: ctypes.winapi_abi, michael@0: ctypes.int32_t, /* success */ michael@0: ctypes.void_t.ptr); /* handle */ michael@0: CloseHandle(handle); michael@0: lib.close(); michael@0: } michael@0: michael@0: /** michael@0: * Creates a mutex. michael@0: * michael@0: * @param aName michael@0: * The name for the mutex. michael@0: * @param aAllowExisting michael@0: * If false the function will close the handle and return null. michael@0: * @return The Win32 handle to the mutex. michael@0: */ michael@0: function createMutex(aName, aAllowExisting) { michael@0: if (aAllowExisting === undefined) { michael@0: aAllowExisting = true; michael@0: } michael@0: michael@0: const INITIAL_OWN = 1; michael@0: const ERROR_ALREADY_EXISTS = 0xB7; michael@0: var lib = ctypes.open("kernel32.dll"); michael@0: var CreateMutexW = lib.declare("CreateMutexW", michael@0: ctypes.winapi_abi, michael@0: ctypes.void_t.ptr, /* return handle */ michael@0: ctypes.void_t.ptr, /* security attributes */ michael@0: ctypes.int32_t, /* initial owner */ michael@0: ctypes.jschar.ptr); /* name */ michael@0: michael@0: var handle = CreateMutexW(null, INITIAL_OWN, aName); michael@0: var alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS; michael@0: if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) { michael@0: closeHandle(handle); michael@0: handle = null; michael@0: } michael@0: lib.close(); michael@0: michael@0: if (handle && handle.isNull()) michael@0: handle = null; michael@0: michael@0: return handle; michael@0: } michael@0: michael@0: /** michael@0: * Determines a unique mutex name for the installation michael@0: * michael@0: * @param aGlobal true if the function should return a global mutex. A global michael@0: * mutex is valid across different sessions michael@0: * @return Global mutex path michael@0: */ michael@0: function getPerInstallationMutexName(aGlobal) { michael@0: if (aGlobal === undefined) { michael@0: aGobal = true; michael@0: } michael@0: let hasher = Cc["@mozilla.org/security/hash;1"]. michael@0: createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hasher.SHA1); michael@0: michael@0: var exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsILocalFile); michael@0: michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: var data = converter.convertToByteArray(exeFile.path.toLowerCase()); michael@0: michael@0: hasher.update(data, data.length); michael@0: return (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true); michael@0: } michael@0: #endif // XP_WIN michael@0: michael@0: /** michael@0: * Whether or not the current instance has the update mutex. The update mutex michael@0: * gives protection against 2 applications from the same installation updating: michael@0: * 1) Running multiple profiles from the same installation path michael@0: * 2) Running a Metro and Desktop application at the same time from the same michael@0: * path michael@0: * 3) 2 applications running in 2 different user sessions from the same path michael@0: * michael@0: * @return true if this instance holds the update mutex michael@0: */ michael@0: function hasUpdateMutex() { michael@0: #ifdef XP_WIN michael@0: if (!gUpdateMutexHandle) { michael@0: gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false); michael@0: } michael@0: michael@0: return !!gUpdateMutexHandle; michael@0: #else michael@0: return true; michael@0: #endif // XP_WIN michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gCanApplyUpdates", function aus_gCanApplyUpdates() { michael@0: function submitHasPermissionsTelemetryPing(val) { michael@0: try { michael@0: let h = Services.telemetry.getHistogramById("UPDATER_HAS_PERMISSIONS"); michael@0: h.add(+val); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Components.utils.reportError(e); michael@0: } michael@0: } michael@0: michael@0: let useService = false; michael@0: if (shouldUseService() && isServiceInstalled()) { michael@0: // No need to perform directory write checks, the maintenance service will michael@0: // be able to write to all directories. michael@0: LOG("gCanApplyUpdates - bypass the write checks because we'll use the service"); michael@0: useService = true; michael@0: } michael@0: michael@0: if (!useService) { michael@0: try { michael@0: var updateTestFile = getUpdateFile([FILE_PERMS_TEST]); michael@0: LOG("gCanApplyUpdates - testing write access " + updateTestFile.path); michael@0: testWriteAccess(updateTestFile, false); michael@0: #ifdef XP_WIN michael@0: var sysInfo = Cc["@mozilla.org/system-info;1"]. michael@0: getService(Ci.nsIPropertyBag2); michael@0: michael@0: // Example windowsVersion: Windows XP == 5.1 michael@0: var windowsVersion = sysInfo.getProperty("version"); michael@0: LOG("gCanApplyUpdates - windowsVersion = " + windowsVersion); michael@0: michael@0: /** michael@0: # For Vista, updates can be performed to a location requiring admin michael@0: # privileges by requesting elevation via the UAC prompt when launching michael@0: # updater.exe if the appDir is under the Program Files directory michael@0: # (e.g. C:\Program Files\) and UAC is turned on and we can elevate michael@0: # (e.g. user has a split token). michael@0: # michael@0: # Note: this does note attempt to handle the case where UAC is turned on michael@0: # and the installation directory is in a restricted location that michael@0: # requires admin privileges to update other than Program Files. michael@0: */ michael@0: var userCanElevate = false; michael@0: michael@0: if (parseFloat(windowsVersion) >= 6) { michael@0: try { michael@0: var fileLocator = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: // KEY_UPDROOT will fail and throw an exception if michael@0: // appDir is not under the Program Files, so we rely on that michael@0: var dir = fileLocator.get(KEY_UPDROOT, Ci.nsIFile); michael@0: // appDir is under Program Files, so check if the user can elevate michael@0: userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper). michael@0: userCanElevate; michael@0: LOG("gCanApplyUpdates - on Vista, userCanElevate: " + userCanElevate); michael@0: } michael@0: catch (ex) { michael@0: // When the installation directory is not under Program Files, michael@0: // fall through to checking if write access to the michael@0: // installation directory is available. michael@0: LOG("gCanApplyUpdates - on Vista, appDir is not under Program Files"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: # On Windows, we no longer store the update under the app dir. michael@0: # michael@0: # If we are on Windows (including Vista, if we can't elevate) we need to michael@0: # to check that we can create and remove files from the actual app michael@0: # directory (like C:\Program Files\Mozilla Firefox). If we can't michael@0: # (because this user is not an adminstrator, for example) canUpdate() michael@0: # should return false. michael@0: # michael@0: # For Vista, we perform this check to enable updating the application michael@0: # when the user has write access to the installation directory under the michael@0: # following scenarios: michael@0: # 1) the installation directory is not under Program Files michael@0: # (e.g. C:\Program Files) michael@0: # 2) UAC is turned off michael@0: # 3) UAC is turned on and the user is not an admin michael@0: # (e.g. the user does not have a split token) michael@0: # 4) UAC is turned on and the user is already elevated, so they can't be michael@0: # elevated again michael@0: */ michael@0: if (!userCanElevate) { michael@0: // if we're unable to create the test file this will throw an exception. michael@0: var appDirTestFile = getAppBaseDir(); michael@0: appDirTestFile.append(FILE_PERMS_TEST); michael@0: LOG("gCanApplyUpdates - testing write access " + appDirTestFile.path); michael@0: if (appDirTestFile.exists()) michael@0: appDirTestFile.remove(false) michael@0: appDirTestFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); michael@0: appDirTestFile.remove(false); michael@0: } michael@0: #endif //XP_WIN michael@0: } michael@0: catch (e) { michael@0: LOG("gCanApplyUpdates - unable to apply updates. Exception: " + e); michael@0: // No write privileges to install directory michael@0: submitHasPermissionsTelemetryPing(false); michael@0: return false; michael@0: } michael@0: } // if (!useService) michael@0: michael@0: LOG("gCanApplyUpdates - able to apply updates"); michael@0: submitHasPermissionsTelemetryPing(true); michael@0: return true; michael@0: }); michael@0: michael@0: /** michael@0: * Whether or not the application can stage an update. michael@0: * michael@0: * @return true if updates can be staged. michael@0: */ michael@0: function getCanStageUpdates() { michael@0: // If background updates are disabled, then just bail out! michael@0: if (!getPref("getBoolPref", PREF_APP_UPDATE_STAGING_ENABLED, false)) { michael@0: LOG("getCanStageUpdates - staging updates is disabled by preference " + michael@0: PREF_APP_UPDATE_STAGING_ENABLED); michael@0: return false; michael@0: } michael@0: michael@0: #ifdef CHECK_CAN_USE_SERVICE michael@0: if (getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false)) { michael@0: // No need to perform directory write checks, the maintenance service will michael@0: // be able to write to all directories. michael@0: LOG("getCanStageUpdates - able to stage updates because we'll use the service"); michael@0: return true; michael@0: } michael@0: #endif michael@0: michael@0: if (!hasUpdateMutex()) { michael@0: LOG("getCanStageUpdates - unable to apply updates because another " + michael@0: "instance of the application is already handling updates for this " + michael@0: "installation."); michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Whether or not the application can stage an update for the current session. michael@0: * These checks are only performed once per session due to using a lazy getter. michael@0: * michael@0: * @return true if updates can be staged for this session. michael@0: */ michael@0: XPCOMUtils.defineLazyGetter(this, "canStageUpdatesSession", function canStageUpdatesSession() { michael@0: try { michael@0: var updateTestFile = getInstallDirRoot(); michael@0: updateTestFile.append(FILE_PERMS_TEST); michael@0: LOG("canStageUpdatesSession - testing write access " + michael@0: updateTestFile.path); michael@0: testWriteAccess(updateTestFile, true); michael@0: #ifndef XP_MACOSX michael@0: // On all platforms except Mac, we need to test the parent directory as michael@0: // well, as we need to be able to move files in that directory during the michael@0: // replacing step. michael@0: updateTestFile = getInstallDirRoot().parent; michael@0: updateTestFile.append(FILE_PERMS_TEST); michael@0: LOG("canStageUpdatesSession - testing write access " + michael@0: updateTestFile.path); michael@0: updateTestFile.createUnique(Ci.nsILocalFile.DIRECTORY_TYPE, michael@0: FileUtils.PERMS_DIRECTORY); michael@0: updateTestFile.remove(false); michael@0: #endif michael@0: } michael@0: catch (e) { michael@0: LOG("canStageUpdatesSession - unable to stage updates. Exception: " + michael@0: e); michael@0: // No write privileges michael@0: return false; michael@0: } michael@0: michael@0: LOG("canStageUpdatesSession - able to stage updates"); michael@0: return true; michael@0: }); michael@0: michael@0: return canStageUpdatesSession; michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gMetroUpdatesEnabled", function aus_gMetroUpdatesEnabled() { michael@0: #ifdef XP_WIN michael@0: #ifdef MOZ_METRO michael@0: if (Services.metro && Services.metro.immersive) { michael@0: let metroUpdate = getPref("getBoolPref", PREF_APP_UPDATE_METRO_ENABLED, true); michael@0: if (!metroUpdate) { michael@0: LOG("gMetroUpdatesEnabled - unable to automatically check for metro " + michael@0: "updates, disabled by pref"); michael@0: return false; michael@0: } michael@0: } michael@0: #endif michael@0: #endif michael@0: michael@0: return true; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gCanCheckForUpdates", function aus_gCanCheckForUpdates() { michael@0: // If the administrator has disabled app update and locked the preference so michael@0: // users can't check for updates. This preference check is ok in this lazy michael@0: // getter since locked prefs don't change until the application is restarted. michael@0: var enabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); michael@0: if (!enabled && Services.prefs.prefIsLocked(PREF_APP_UPDATE_ENABLED)) { michael@0: LOG("gCanCheckForUpdates - unable to automatically check for updates, " + michael@0: "the preference is disabled and admistratively locked."); michael@0: return false; michael@0: } michael@0: michael@0: if (!gMetroUpdatesEnabled) { michael@0: return false; michael@0: } michael@0: michael@0: // If we don't know the binary platform we're updating, we can't update. michael@0: if (!gABI) { michael@0: LOG("gCanCheckForUpdates - unable to check for updates, unknown ABI"); michael@0: return false; michael@0: } michael@0: michael@0: // If we don't know the OS version we're updating, we can't update. michael@0: if (!gOSVersion) { michael@0: LOG("gCanCheckForUpdates - unable to check for updates, unknown OS " + michael@0: "version"); michael@0: return false; michael@0: } michael@0: michael@0: LOG("gCanCheckForUpdates - able to check for updates"); michael@0: return true; michael@0: }); michael@0: michael@0: /** michael@0: * Logs a string to the error console. michael@0: * @param string michael@0: * The string to write to the error console. michael@0: */ michael@0: function LOG(string) { michael@0: if (gLogEnabled) { michael@0: dump("*** AUS:SVC " + string + "\n"); michael@0: Services.console.logStringMessage("AUS:SVC " + string); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: # Gets a preference value, handling the case where there is no default. michael@0: # @param func michael@0: # The name of the preference function to call, on nsIPrefBranch michael@0: # @param preference michael@0: # The name of the preference michael@0: # @param defaultValue michael@0: # The default value to return in the event the preference has michael@0: # no setting michael@0: # @return The value of the preference, or undefined if there was no michael@0: # user or default value. michael@0: */ michael@0: function getPref(func, preference, defaultValue) { michael@0: try { michael@0: return Services.prefs[func](preference); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return defaultValue; michael@0: } michael@0: michael@0: /** michael@0: * Convert a string containing binary values to hex. michael@0: */ michael@0: function binaryToHex(input) { michael@0: var result = ""; michael@0: for (var i = 0; i < input.length; ++i) { michael@0: var hex = input.charCodeAt(i).toString(16); michael@0: if (hex.length == 1) michael@0: hex = "0" + hex; michael@0: result += hex; michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: # Gets the specified directory at the specified hierarchy under the michael@0: # update root directory and creates it if it doesn't exist. michael@0: # @param pathArray michael@0: # An array of path components to locate beneath the directory michael@0: # specified by |key| michael@0: # @return nsIFile object for the location specified. michael@0: */ michael@0: function getUpdateDirCreate(pathArray) { michael@0: return FileUtils.getDir(KEY_UPDROOT, pathArray, true); michael@0: } michael@0: michael@0: /** michael@0: # Gets the specified directory at the specified hierarchy under the michael@0: # update root directory and without creating it if it doesn't exist. michael@0: # @param pathArray michael@0: # An array of path components to locate beneath the directory michael@0: # specified by |key| michael@0: # @return nsIFile object for the location specified. michael@0: */ michael@0: function getUpdateDirNoCreate(pathArray) { michael@0: return FileUtils.getDir(KEY_UPDROOT, pathArray, false); michael@0: } michael@0: michael@0: /** michael@0: * Gets the application base directory. michael@0: * michael@0: * @return nsIFile object for the application base directory. michael@0: */ michael@0: function getAppBaseDir() { michael@0: return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent; michael@0: } michael@0: michael@0: /** michael@0: * Gets the root of the installation directory which is the application michael@0: * bundle directory on Mac OS X and the location of the application binary michael@0: * on all other platforms. michael@0: * michael@0: * @return nsIFile object for the directory michael@0: */ michael@0: function getInstallDirRoot() { michael@0: var dir = getAppBaseDir(); michael@0: #ifdef XP_MACOSX michael@0: // On Mac, we store the Updated.app directory inside the bundle directory. michael@0: dir = dir.parent.parent; michael@0: #endif michael@0: return dir; michael@0: } michael@0: michael@0: /** michael@0: * Gets the file at the specified hierarchy under the update root directory. michael@0: * @param pathArray michael@0: * An array of path components to locate beneath the directory michael@0: * specified by |key|. The last item in this array must be the michael@0: * leaf name of a file. michael@0: * @return nsIFile object for the file specified. The file is NOT created michael@0: * if it does not exist, however all required directories along michael@0: * the way are. michael@0: */ michael@0: function getUpdateFile(pathArray) { michael@0: var file = getUpdateDirCreate(pathArray.slice(0, -1)); michael@0: file.append(pathArray[pathArray.length - 1]); michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * Returns human readable status text from the updates.properties bundle michael@0: * based on an error code michael@0: * @param code michael@0: * The error code to look up human readable status text for michael@0: * @param defaultCode michael@0: * The default code to look up should human readable status text michael@0: * not exist for |code| michael@0: * @return A human readable status text string michael@0: */ michael@0: function getStatusTextFromCode(code, defaultCode) { michael@0: var reason; michael@0: try { michael@0: reason = gUpdateBundle.GetStringFromName("check_error-" + code); michael@0: LOG("getStatusTextFromCode - transfer error: " + reason + ", code: " + michael@0: code); michael@0: } michael@0: catch (e) { michael@0: // Use the default reason michael@0: reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode); michael@0: LOG("getStatusTextFromCode - transfer error: " + reason + michael@0: ", default code: " + defaultCode); michael@0: } michael@0: return reason; michael@0: } michael@0: michael@0: /** michael@0: * Get the Active Updates directory michael@0: * @return The active updates directory, as a nsIFile object michael@0: */ michael@0: function getUpdatesDir() { michael@0: // Right now, we only support downloading one patch at a time, so we always michael@0: // use the same target directory. michael@0: return getUpdateDirCreate([DIR_UPDATES, "0"]); michael@0: } michael@0: michael@0: /** michael@0: * Get the Active Updates directory inside the directory where we apply the michael@0: * background updates. michael@0: * @return The active updates directory inside the updated directory, as a michael@0: * nsIFile object. michael@0: */ michael@0: function getUpdatesDirInApplyToDir() { michael@0: var dir = getAppBaseDir(); michael@0: #ifdef XP_MACOSX michael@0: dir = dir.parent.parent; // the bundle directory michael@0: #endif michael@0: dir.append(UPDATED_DIR); michael@0: #ifdef XP_MACOSX michael@0: dir.append("Contents"); michael@0: dir.append("MacOS"); michael@0: #endif michael@0: dir.append(DIR_UPDATES); michael@0: if (!dir.exists()) { michael@0: dir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: } michael@0: return dir; michael@0: } michael@0: michael@0: /** michael@0: * Reads the update state from the update.status file in the specified michael@0: * directory. michael@0: * @param dir michael@0: * The dir to look for an update.status file in michael@0: * @return The status value of the update. michael@0: */ michael@0: function readStatusFile(dir) { michael@0: var statusFile = dir.clone(); michael@0: statusFile.append(FILE_UPDATE_STATUS); michael@0: var status = readStringFromFile(statusFile) || STATE_NONE; michael@0: LOG("readStatusFile - status: " + status + ", path: " + statusFile.path); michael@0: return status; michael@0: } michael@0: michael@0: /** michael@0: * Writes the current update operation/state to a file in the patch michael@0: * directory, indicating to the patching system that operations need michael@0: * to be performed. michael@0: * @param dir michael@0: * The patch directory where the update.status file should be michael@0: * written. michael@0: * @param state michael@0: * The state value to write. michael@0: */ michael@0: function writeStatusFile(dir, state) { michael@0: var statusFile = dir.clone(); michael@0: statusFile.append(FILE_UPDATE_STATUS); michael@0: writeStringToFile(statusFile, state); michael@0: } michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: /** michael@0: * Reads the link file specified in the update.link file in the michael@0: * specified directory and returns the nsIFile for the michael@0: * corresponding file. michael@0: * @param dir michael@0: * The dir to look for an update.link file in michael@0: * @return A nsIFile for the file path specified in the michael@0: * update.link file or null if the update.link file michael@0: * doesn't exist. michael@0: */ michael@0: function getFileFromUpdateLink(dir) { michael@0: var linkFile = dir.clone(); michael@0: linkFile.append(FILE_UPDATE_LINK); michael@0: var link = readStringFromFile(linkFile); michael@0: LOG("getFileFromUpdateLink linkFile.path: " + linkFile.path + ", link: " + link); michael@0: if (!link) { michael@0: return null; michael@0: } michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.initWithPath(link); michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * Creates a link file, which allows the actual patch to live in michael@0: * a directory different from the update directory. michael@0: * @param dir michael@0: * The patch directory where the update.link file michael@0: * should be written. michael@0: * @param patchFile michael@0: * The fully qualified filename of the patchfile. michael@0: */ michael@0: function writeLinkFile(dir, patchFile) { michael@0: var linkFile = dir.clone(); michael@0: linkFile.append(FILE_UPDATE_LINK); michael@0: writeStringToFile(linkFile, patchFile.path); michael@0: if (patchFile.path.indexOf(gExtStorage) == 0) { michael@0: // The patchfile is being stored on external storage. Try to lock it michael@0: // so that it doesn't get shared with the PC while we're downloading michael@0: // to it. michael@0: acquireSDCardMountLock(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Acquires a VolumeMountLock for the sdcard volume. michael@0: * michael@0: * This prevents the SDCard from being shared with the PC while michael@0: * we're downloading the update. michael@0: */ michael@0: function acquireSDCardMountLock() { michael@0: let volsvc = Cc["@mozilla.org/telephony/volume-service;1"]. michael@0: getService(Ci.nsIVolumeService); michael@0: if (volsvc) { michael@0: gSDCardMountLock = volsvc.createMountLock("sdcard"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Determines if the state corresponds to an interrupted update. michael@0: * This could either be because the download was interrupted, or michael@0: * because staging the update was interrupted. michael@0: * michael@0: * @return true if the state corresponds to an interrupted michael@0: * update. michael@0: */ michael@0: function isInterruptedUpdate(status) { michael@0: return (status == STATE_DOWNLOADING) || michael@0: (status == STATE_PENDING) || michael@0: (status == STATE_APPLYING); michael@0: } michael@0: #endif // MOZ_WIDGET_GONK michael@0: michael@0: /** michael@0: * Releases any SDCard mount lock that we might have. michael@0: * michael@0: * This once again allows the SDCard to be shared with the PC. michael@0: * michael@0: * This function was placed outside the #ifdef so that we didn't michael@0: * need to put #ifdefs around the callers. michael@0: */ michael@0: function releaseSDCardMountLock() { michael@0: #ifdef MOZ_WIDGET_GONK michael@0: if (gSDCardMountLock) { michael@0: gSDCardMountLock.unlock(); michael@0: gSDCardMountLock = null; michael@0: } michael@0: #endif michael@0: } michael@0: michael@0: /** michael@0: * Determines if the service should be used to attempt an update michael@0: * or not. For now this is only when PREF_APP_UPDATE_SERVICE_ENABLED michael@0: * is true and we have Firefox. michael@0: * michael@0: * @return true if the service should be used for updates. michael@0: */ michael@0: function shouldUseService() { michael@0: #ifdef MOZ_MAINTENANCE_SERVICE michael@0: return getPref("getBoolPref", michael@0: PREF_APP_UPDATE_SERVICE_ENABLED, false); michael@0: #else michael@0: return false; michael@0: #endif michael@0: } michael@0: michael@0: /** michael@0: * Determines if the service is is installed and enabled or not. michael@0: * michael@0: * @return true if the service should be used for updates, michael@0: * is installed and enabled. michael@0: */ michael@0: function isServiceInstalled() { michael@0: #ifdef XP_WIN michael@0: let installed = 0; michael@0: try { michael@0: let wrk = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, michael@0: "SOFTWARE\\Mozilla\\MaintenanceService", michael@0: wrk.ACCESS_READ | wrk.WOW64_64); michael@0: installed = wrk.readIntValue("Installed"); michael@0: wrk.close(); michael@0: } catch(e) { michael@0: } michael@0: installed = installed == 1; // convert to bool michael@0: LOG("isServiceInstalled = " + installed); michael@0: return installed; michael@0: #else michael@0: return false; michael@0: #endif michael@0: } michael@0: michael@0: /** michael@0: # Writes the update's application version to a file in the patch directory. If michael@0: # the update doesn't provide application version information via the michael@0: # appVersion attribute the string "null" will be written to the file. michael@0: # This value is compared during startup (in nsUpdateDriver.cpp) to determine if michael@0: # the update should be applied. Note that this won't provide protection from michael@0: # downgrade of the application for the nightly user case where the application michael@0: # version doesn't change. michael@0: # @param dir michael@0: # The patch directory where the update.version file should be michael@0: # written. michael@0: # @param version michael@0: # The version value to write. Will be the string "null" when the michael@0: # update doesn't provide the appVersion attribute in the update xml. michael@0: */ michael@0: function writeVersionFile(dir, version) { michael@0: var versionFile = dir.clone(); michael@0: versionFile.append(FILE_UPDATE_VERSION); michael@0: writeStringToFile(versionFile, version); michael@0: } michael@0: michael@0: /** michael@0: * Removes the MozUpdater folders that bgupdates/staged updates creates. michael@0: */ michael@0: function cleanUpMozUpdaterDirs() { michael@0: try { michael@0: var tmpDir = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties). michael@0: get("TmpD", Ci.nsIFile); michael@0: michael@0: // We used to store MozUpdater-i folders directly inside the temp directory. michael@0: // We need to cleanup these directories if we detect that they still exist. michael@0: // To check if they still exist, we simply check for MozUpdater-1. michael@0: var mozUpdaterDir1 = tmpDir.clone(); michael@0: mozUpdaterDir1.append("MozUpdater-1"); michael@0: // Only try to delete the left over folders in "$Temp/MozUpdater-i/*" if michael@0: // MozUpdater-1 exists. michael@0: if (mozUpdaterDir1.exists()) { michael@0: LOG("cleanUpMozUpdaterDirs - Cleaning top level MozUpdater-i folders"); michael@0: let i = 0; michael@0: let dirEntries = tmpDir.directoryEntries; michael@0: while (dirEntries.hasMoreElements() && i < 10) { michael@0: let file = dirEntries.getNext().QueryInterface(Ci.nsILocalFile); michael@0: if (file.leafName.startsWith("MozUpdater-") && file.leafName != "MozUpdater-1") { michael@0: file.remove(true); michael@0: i++; michael@0: } michael@0: } michael@0: // If you enumerate the whole temp directory and the count of deleted michael@0: // items is less than 10, then delete MozUpdate-1. michael@0: if (i < 10) { michael@0: mozUpdaterDir1.remove(true); michael@0: } michael@0: } michael@0: michael@0: // If we reach here, we simply need to clean the MozUpdater folder. In our michael@0: // new way of storing these files, the unique subfolders are inside MozUpdater michael@0: var mozUpdaterDir = tmpDir.clone(); michael@0: mozUpdaterDir.append("MozUpdater"); michael@0: if (mozUpdaterDir.exists()) { michael@0: LOG("cleanUpMozUpdaterDirs - Cleaning MozUpdater folder"); michael@0: mozUpdaterDir.remove(true); michael@0: } michael@0: } catch (e) { michael@0: LOG("cleanUpMozUpdaterDirs - Exception: " + e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Removes the contents of the Updates Directory michael@0: * michael@0: * @param aBackgroundUpdate Whether the update has been performed in the michael@0: * background. If this is true, we move the update log file to the michael@0: * updated directory, so that it survives replacing the directories michael@0: * later on. michael@0: */ michael@0: function cleanUpUpdatesDir(aBackgroundUpdate) { michael@0: // Bail out if we don't have appropriate permissions michael@0: try { michael@0: var updateDir = getUpdatesDir(); michael@0: } catch (e) { michael@0: return; michael@0: } michael@0: michael@0: // Preserve the last update log file for debugging purposes. michael@0: let file = updateDir.clone(); michael@0: file.append(FILE_UPDATE_LOG); michael@0: if (file.exists()) { michael@0: let dir; michael@0: if (aBackgroundUpdate && getUpdateDirNoCreate([]).equals(getAppBaseDir())) { michael@0: dir = getUpdatesDirInApplyToDir(); michael@0: } else { michael@0: dir = updateDir.parent; michael@0: } michael@0: let logFile = dir.clone(); michael@0: logFile.append(FILE_LAST_LOG); michael@0: if (logFile.exists()) { michael@0: try { michael@0: logFile.moveTo(dir, FILE_BACKUP_LOG); michael@0: } catch (e) { michael@0: LOG("cleanUpUpdatesDir - failed to rename file " + logFile.path + michael@0: " to " + FILE_BACKUP_LOG); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: file.moveTo(dir, FILE_LAST_LOG); michael@0: } catch (e) { michael@0: LOG("cleanUpUpdatesDir - failed to rename file " + file.path + michael@0: " to " + FILE_LAST_LOG); michael@0: } michael@0: } michael@0: michael@0: if (!aBackgroundUpdate) { michael@0: let e = updateDir.directoryEntries; michael@0: while (e.hasMoreElements()) { michael@0: let f = e.getNext().QueryInterface(Ci.nsIFile); michael@0: #ifdef MOZ_WIDGET_GONK michael@0: if (f.leafName == FILE_UPDATE_LINK) { michael@0: let linkedFile = getFileFromUpdateLink(updateDir); michael@0: if (linkedFile && linkedFile.exists()) { michael@0: linkedFile.remove(false); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: // Now, recursively remove this file. The recursive removal is needed for michael@0: // Mac OSX because this directory will contain a copy of updater.app, michael@0: // which is itself a directory. michael@0: try { michael@0: f.remove(true); michael@0: } catch (e) { michael@0: LOG("cleanUpUpdatesDir - failed to remove file " + f.path); michael@0: } michael@0: } michael@0: } michael@0: releaseSDCardMountLock(); michael@0: } michael@0: michael@0: /** michael@0: * Clean up updates list and the updates directory. michael@0: */ michael@0: function cleanupActiveUpdate() { michael@0: // Move the update from the Active Update list into the Past Updates list. michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: um.activeUpdate = null; michael@0: um.saveUpdates(); michael@0: michael@0: // Now trash the updates directory, since we're done with it michael@0: cleanUpUpdatesDir(); michael@0: } michael@0: michael@0: /** michael@0: * Gets the locale from the update.locale file for replacing %LOCALE% in the michael@0: * update url. The update.locale file can be located in the application michael@0: * directory or the GRE directory with preference given to it being located in michael@0: * the application directory. michael@0: */ michael@0: function getLocale() { michael@0: if (gLocale) michael@0: return gLocale; michael@0: michael@0: for (let res of ['app', 'gre']) { michael@0: var channel = Services.io.newChannel("resource://" + res + "/" + FILE_UPDATE_LOCALE, null, null); michael@0: try { michael@0: var inputStream = channel.open(); michael@0: gLocale = readStringFromInputStream(inputStream); michael@0: } catch(e) {} michael@0: if (gLocale) michael@0: break; michael@0: } michael@0: michael@0: if (!gLocale) michael@0: throw Components.Exception(FILE_UPDATE_LOCALE + " file doesn't exist in " + michael@0: "either the application or GRE directories", michael@0: Cr.NS_ERROR_FILE_NOT_FOUND); michael@0: michael@0: LOG("getLocale - getting locale from file: " + channel.originalURI.spec + michael@0: ", locale: " + gLocale); michael@0: return gLocale; michael@0: } michael@0: michael@0: /* Get the distribution pref values, from defaults only */ michael@0: function getDistributionPrefValue(aPrefName) { michael@0: var prefValue = "default"; michael@0: michael@0: try { michael@0: prefValue = Services.prefs.getDefaultBranch(null).getCharPref(aPrefName); michael@0: } catch (e) { michael@0: // use default when pref not found michael@0: } michael@0: michael@0: return prefValue; michael@0: } michael@0: michael@0: /** michael@0: * An enumeration of items in a JS array. michael@0: * @constructor michael@0: */ michael@0: function ArrayEnumerator(aItems) { michael@0: this._index = 0; michael@0: if (aItems) { michael@0: for (var i = 0; i < aItems.length; ++i) { michael@0: if (!aItems[i]) michael@0: aItems.splice(i, 1); michael@0: } michael@0: } michael@0: this._contents = aItems; michael@0: } michael@0: michael@0: ArrayEnumerator.prototype = { michael@0: _index: 0, michael@0: _contents: [], michael@0: michael@0: hasMoreElements: function ArrayEnumerator_hasMoreElements() { michael@0: return this._index < this._contents.length; michael@0: }, michael@0: michael@0: getNext: function ArrayEnumerator_getNext() { michael@0: return this._contents[this._index++]; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Writes a string of text to a file. A newline will be appended to the data michael@0: * written to the file. This function only works with ASCII text. michael@0: */ michael@0: function writeStringToFile(file, text) { michael@0: var fos = FileUtils.openSafeFileOutputStream(file) michael@0: text += "\n"; michael@0: fos.write(text, text.length); michael@0: FileUtils.closeSafeFileOutputStream(fos); michael@0: } michael@0: michael@0: function readStringFromInputStream(inputStream) { michael@0: var sis = Cc["@mozilla.org/scriptableinputstream;1"]. michael@0: createInstance(Ci.nsIScriptableInputStream); michael@0: sis.init(inputStream); michael@0: var text = sis.read(sis.available()); michael@0: sis.close(); michael@0: if (text[text.length - 1] == "\n") michael@0: text = text.slice(0, -1); michael@0: return text; michael@0: } michael@0: michael@0: /** michael@0: * Reads a string of text from a file. A trailing newline will be removed michael@0: * before the result is returned. This function only works with ASCII text. michael@0: */ michael@0: function readStringFromFile(file) { michael@0: if (!file.exists()) { michael@0: LOG("readStringFromFile - file doesn't exist: " + file.path); michael@0: return null; michael@0: } michael@0: var fis = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); michael@0: return readStringFromInputStream(fis); michael@0: } michael@0: michael@0: function handleUpdateFailure(update, errorCode) { michael@0: update.errorCode = parseInt(errorCode); michael@0: if (update.errorCode == FOTA_GENERAL_ERROR || michael@0: update.errorCode == FOTA_FILE_OPERATION_ERROR || michael@0: update.errorCode == FOTA_RECOVERY_ERROR || michael@0: update.errorCode == FOTA_UNKNOWN_ERROR) { michael@0: // In the case of FOTA update errors, don't reset the state to pending. This michael@0: // causes the FOTA update path to try again, which is not necessarily what michael@0: // we want. michael@0: update.statusText = gUpdateBundle.GetStringFromName("statusFailed"); michael@0: michael@0: Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt). michael@0: showUpdateError(update); michael@0: writeStatusFile(getUpdatesDir(), STATE_FAILED + ": " + errorCode); michael@0: cleanupActiveUpdate(); michael@0: return true; michael@0: } michael@0: michael@0: if (update.errorCode == WRITE_ERROR || michael@0: update.errorCode == WRITE_ERROR_ACCESS_DENIED || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_SIGNALED || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_NOPID || michael@0: update.errorCode == WRITE_ERROR_CALLBACK_APP || michael@0: update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR) { michael@0: Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt). michael@0: showUpdateError(update); michael@0: writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); michael@0: return true; michael@0: } michael@0: michael@0: if (update.errorCode == ELEVATION_CANCELED) { michael@0: writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); michael@0: return true; michael@0: } michael@0: michael@0: if (update.errorCode == SERVICE_UPDATER_COULD_NOT_BE_STARTED || michael@0: update.errorCode == SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS || michael@0: update.errorCode == SERVICE_UPDATER_SIGN_ERROR || michael@0: update.errorCode == SERVICE_UPDATER_COMPARE_ERROR || michael@0: update.errorCode == SERVICE_UPDATER_IDENTITY_ERROR || michael@0: update.errorCode == SERVICE_STILL_APPLYING_ON_SUCCESS || michael@0: update.errorCode == SERVICE_STILL_APPLYING_ON_FAILURE || michael@0: update.errorCode == SERVICE_UPDATER_NOT_FIXED_DRIVE || michael@0: update.errorCode == SERVICE_COULD_NOT_LOCK_UPDATER || michael@0: update.errorCode == SERVICE_COULD_NOT_COPY_UPDATER || michael@0: update.errorCode == SERVICE_INSTALLDIR_ERROR) { michael@0: michael@0: var failCount = getPref("getIntPref", michael@0: PREF_APP_UPDATE_SERVICE_ERRORS, 0); michael@0: var maxFail = getPref("getIntPref", michael@0: PREF_APP_UPDATE_SERVICE_MAX_ERRORS, michael@0: DEFAULT_SERVICE_MAX_ERRORS); michael@0: michael@0: // As a safety, when the service reaches maximum failures, it will michael@0: // disable itself and fallback to using the normal update mechanism michael@0: // without the service. michael@0: if (failCount >= maxFail) { michael@0: Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); michael@0: Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); michael@0: } else { michael@0: failCount++; michael@0: Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, michael@0: failCount); michael@0: } michael@0: michael@0: writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); michael@0: try { michael@0: Services.telemetry.getHistogramById("UPDATER_SERVICE_ERROR_CODE"). michael@0: add(update.errorCode); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: try { michael@0: Services.telemetry.getHistogramById("UPDATER_SERVICE_ERROR_CODE").add(0); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Fall back to downloading a complete update in case an update has failed. michael@0: * michael@0: * @param update the update object that has failed to apply. michael@0: * @param postStaging true if we have just attempted to stage an update. michael@0: */ michael@0: function handleFallbackToCompleteUpdate(update, postStaging) { michael@0: cleanupActiveUpdate(); michael@0: michael@0: update.statusText = gUpdateBundle.GetStringFromName("patchApplyFailure"); michael@0: var oldType = update.selectedPatch ? update.selectedPatch.type michael@0: : "complete"; michael@0: if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) { michael@0: // Partial patch application failed, try downloading the complete michael@0: // update in the background instead. michael@0: LOG("handleFallbackToCompleteUpdate - install of partial patch " + michael@0: "failed, downloading complete patch"); michael@0: var status = Cc["@mozilla.org/updates/update-service;1"]. michael@0: getService(Ci.nsIApplicationUpdateService). michael@0: downloadUpdate(update, !postStaging); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: } michael@0: else { michael@0: LOG("handleFallbackToCompleteUpdate - install of complete or " + michael@0: "only one patch offered failed."); michael@0: } michael@0: update.QueryInterface(Ci.nsIWritablePropertyBag); michael@0: update.setProperty("patchingFailed", oldType); michael@0: } michael@0: michael@0: /** michael@0: * Update Patch michael@0: * @param patch michael@0: * A element to initialize this object with michael@0: * @throws if patch has a size of 0 michael@0: * @constructor michael@0: */ michael@0: function UpdatePatch(patch) { michael@0: this._properties = {}; michael@0: for (var i = 0; i < patch.attributes.length; ++i) { michael@0: var attr = patch.attributes.item(i); michael@0: attr.QueryInterface(Ci.nsIDOMAttr); michael@0: switch (attr.name) { michael@0: case "selected": michael@0: this.selected = attr.value == "true"; michael@0: break; michael@0: case "size": michael@0: if (0 == parseInt(attr.value)) { michael@0: LOG("UpdatePatch:init - 0-sized patch!"); michael@0: throw Cr.NS_ERROR_ILLEGAL_VALUE; michael@0: } michael@0: // fall through michael@0: default: michael@0: this[attr.name] = attr.value; michael@0: break; michael@0: }; michael@0: } michael@0: } michael@0: UpdatePatch.prototype = { michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: serialize: function UpdatePatch_serialize(updates) { michael@0: var patch = updates.createElementNS(URI_UPDATE_NS, "patch"); michael@0: patch.setAttribute("type", this.type); michael@0: patch.setAttribute("URL", this.URL); michael@0: // finalURL is not available until after the download has started michael@0: if (this.finalURL) michael@0: patch.setAttribute("finalURL", this.finalURL); michael@0: patch.setAttribute("hashFunction", this.hashFunction); michael@0: patch.setAttribute("hashValue", this.hashValue); michael@0: patch.setAttribute("size", this.size); michael@0: patch.setAttribute("selected", this.selected); michael@0: patch.setAttribute("state", this.state); michael@0: michael@0: for (var p in this._properties) { michael@0: if (this._properties[p].present) michael@0: patch.setAttribute(p, this._properties[p].data); michael@0: } michael@0: michael@0: return patch; michael@0: }, michael@0: michael@0: /** michael@0: * A hash of custom properties michael@0: */ michael@0: _properties: null, michael@0: michael@0: /** michael@0: * See nsIWritablePropertyBag.idl michael@0: */ michael@0: setProperty: function UpdatePatch_setProperty(name, value) { michael@0: this._properties[name] = { data: value, present: true }; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIWritablePropertyBag.idl michael@0: */ michael@0: deleteProperty: function UpdatePatch_deleteProperty(name) { michael@0: if (name in this._properties) michael@0: this._properties[name].present = false; michael@0: else michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIPropertyBag.idl michael@0: */ michael@0: get enumerator() { michael@0: var properties = []; michael@0: for (var p in this._properties) michael@0: properties.push(this._properties[p].data); michael@0: return new ArrayEnumerator(properties); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIPropertyBag.idl michael@0: */ michael@0: getProperty: function UpdatePatch_getProperty(name) { michael@0: if (name in this._properties && michael@0: this._properties[name].present) michael@0: return this._properties[name].data; michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether or not the update.status file for this patch exists at the michael@0: * appropriate location. michael@0: */ michael@0: get statusFileExists() { michael@0: var statusFile = getUpdatesDir(); michael@0: statusFile.append(FILE_UPDATE_STATUS); michael@0: return statusFile.exists(); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get state() { michael@0: if (this._properties.state) michael@0: return this._properties.state; michael@0: return STATE_NONE; michael@0: }, michael@0: set state(val) { michael@0: this._properties.state = val; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePatch, michael@0: Ci.nsIPropertyBag, michael@0: Ci.nsIWritablePropertyBag]) michael@0: }; michael@0: michael@0: /** michael@0: * Update michael@0: * Implements nsIUpdate michael@0: * @param update michael@0: * An element to initialize this object with michael@0: * @throws if the update contains no patches michael@0: * @constructor michael@0: */ michael@0: function Update(update) { michael@0: this._properties = {}; michael@0: this._patches = []; michael@0: this.isCompleteUpdate = false; michael@0: this.isOSUpdate = false; michael@0: this.showPrompt = false; michael@0: this.showNeverForVersion = false; michael@0: this.unsupported = false; michael@0: this.channel = "default"; michael@0: this.promptWaitTime = getPref("getIntPref", PREF_APP_UPDATE_PROMPTWAITTIME, 43200); michael@0: michael@0: // Null , assume this is a message container and do no michael@0: // further initialization michael@0: if (!update) michael@0: return; michael@0: michael@0: const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; michael@0: for (var i = 0; i < update.childNodes.length; ++i) { michael@0: var patchElement = update.childNodes.item(i); michael@0: if (patchElement.nodeType != ELEMENT_NODE || michael@0: patchElement.localName != "patch") michael@0: continue; michael@0: michael@0: patchElement.QueryInterface(Ci.nsIDOMElement); michael@0: try { michael@0: var patch = new UpdatePatch(patchElement); michael@0: } catch (e) { michael@0: continue; michael@0: } michael@0: this._patches.push(patch); michael@0: } michael@0: michael@0: if (update.hasAttribute("unsupported")) michael@0: this.unsupported = ("true" == update.getAttribute("unsupported")); michael@0: else if (update.hasAttribute("minSupportedOSVersion")) { michael@0: let minOSVersion = update.getAttribute("minSupportedOSVersion"); michael@0: try { michael@0: let osVersion = Services.sysinfo.getProperty("version"); michael@0: this.unsupported = (Services.vc.compare(osVersion, minOSVersion) < 0); michael@0: } catch (e) {} michael@0: } michael@0: michael@0: if (this._patches.length == 0 && !this.unsupported) michael@0: throw Cr.NS_ERROR_ILLEGAL_VALUE; michael@0: michael@0: // Fallback to the behavior prior to bug 530872 if the update does not have an michael@0: // appVersion attribute. michael@0: if (!update.hasAttribute("appVersion")) { michael@0: if (update.getAttribute("type") == "major") { michael@0: if (update.hasAttribute("detailsURL")) { michael@0: this.billboardURL = update.getAttribute("detailsURL"); michael@0: this.showPrompt = true; michael@0: this.showNeverForVersion = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: for (var i = 0; i < update.attributes.length; ++i) { michael@0: var attr = update.attributes.item(i); michael@0: attr.QueryInterface(Ci.nsIDOMAttr); michael@0: if (attr.value == "undefined") michael@0: continue; michael@0: else if (attr.name == "detailsURL") michael@0: this._detailsURL = attr.value; michael@0: else if (attr.name == "extensionVersion") { michael@0: // Prevent extensionVersion from replacing appVersion if appVersion is michael@0: // present in the update xml. michael@0: if (!this.appVersion) michael@0: this.appVersion = attr.value; michael@0: } michael@0: else if (attr.name == "installDate" && attr.value) michael@0: this.installDate = parseInt(attr.value); michael@0: else if (attr.name == "isCompleteUpdate") michael@0: this.isCompleteUpdate = attr.value == "true"; michael@0: else if (attr.name == "isSecurityUpdate") michael@0: this.isSecurityUpdate = attr.value == "true"; michael@0: else if (attr.name == "isOSUpdate") michael@0: this.isOSUpdate = attr.value == "true"; michael@0: else if (attr.name == "showNeverForVersion") michael@0: this.showNeverForVersion = attr.value == "true"; michael@0: else if (attr.name == "showPrompt") michael@0: this.showPrompt = attr.value == "true"; michael@0: else if (attr.name == "promptWaitTime") michael@0: { michael@0: if(!isNaN(attr.value)) michael@0: this.promptWaitTime = parseInt(attr.value); michael@0: } michael@0: else if (attr.name == "version") { michael@0: // Prevent version from replacing displayVersion if displayVersion is michael@0: // present in the update xml. michael@0: if (!this.displayVersion) michael@0: this.displayVersion = attr.value; michael@0: } michael@0: else if (attr.name != "unsupported") { michael@0: this[attr.name] = attr.value; michael@0: michael@0: switch (attr.name) { michael@0: case "appVersion": michael@0: case "billboardURL": michael@0: case "buildID": michael@0: case "channel": michael@0: case "displayVersion": michael@0: case "licenseURL": michael@0: case "name": michael@0: case "platformVersion": michael@0: case "previousAppVersion": michael@0: case "serviceURL": michael@0: case "statusText": michael@0: case "type": michael@0: break; michael@0: default: michael@0: // Save custom attributes when serializing to the local xml file but michael@0: // don't use this method for the expected attributes which are already michael@0: // handled in serialize. michael@0: this.setProperty(attr.name, attr.value); michael@0: break; michael@0: }; michael@0: } michael@0: } michael@0: michael@0: // Set the initial value with the current time when it doesn't already have a michael@0: // value or the value is already set to 0 (bug 316328). michael@0: if (!this.installDate && this.installDate != 0) michael@0: this.installDate = (new Date()).getTime(); michael@0: michael@0: // The Update Name is either the string provided by the element, or michael@0: // the string: " " michael@0: var name = ""; michael@0: if (update.hasAttribute("name")) michael@0: name = update.getAttribute("name"); michael@0: else { michael@0: var brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); michael@0: var appName = brandBundle.GetStringFromName("brandShortName"); michael@0: name = gUpdateBundle.formatStringFromName("updateName", michael@0: [appName, this.displayVersion], 2); michael@0: } michael@0: this.name = name; michael@0: } michael@0: Update.prototype = { michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get patchCount() { michael@0: return this._patches.length; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: getPatchAt: function Update_getPatchAt(index) { michael@0: return this._patches[index]; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: * michael@0: * We use a copy of the state cached on this object in |_state| only when michael@0: * there is no selected patch, i.e. in the case when we could not load michael@0: * |.activeUpdate| from the update manager for some reason but still have michael@0: * the update.status file to work with. michael@0: */ michael@0: _state: "", michael@0: set state(state) { michael@0: if (this.selectedPatch) michael@0: this.selectedPatch.state = state; michael@0: this._state = state; michael@0: return state; michael@0: }, michael@0: get state() { michael@0: if (this.selectedPatch) michael@0: return this.selectedPatch.state; michael@0: return this._state; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: errorCode: 0, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get selectedPatch() { michael@0: for (var i = 0; i < this.patchCount; ++i) { michael@0: if (this._patches[i].selected) michael@0: return this._patches[i]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get detailsURL() { michael@0: if (!this._detailsURL) { michael@0: try { michael@0: // Try using a default details URL supplied by the distribution michael@0: // if the update XML does not supply one. michael@0: return Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS); michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: return this._detailsURL || ""; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: serialize: function Update_serialize(updates) { michael@0: var update = updates.createElementNS(URI_UPDATE_NS, "update"); michael@0: update.setAttribute("appVersion", this.appVersion); michael@0: update.setAttribute("buildID", this.buildID); michael@0: update.setAttribute("channel", this.channel); michael@0: update.setAttribute("displayVersion", this.displayVersion); michael@0: // for backwards compatibility in case the user downgrades michael@0: update.setAttribute("extensionVersion", this.appVersion); michael@0: update.setAttribute("installDate", this.installDate); michael@0: update.setAttribute("isCompleteUpdate", this.isCompleteUpdate); michael@0: update.setAttribute("isOSUpdate", this.isOSUpdate); michael@0: update.setAttribute("name", this.name); michael@0: update.setAttribute("serviceURL", this.serviceURL); michael@0: update.setAttribute("showNeverForVersion", this.showNeverForVersion); michael@0: update.setAttribute("showPrompt", this.showPrompt); michael@0: update.setAttribute("promptWaitTime", this.promptWaitTime); michael@0: update.setAttribute("type", this.type); michael@0: // for backwards compatibility in case the user downgrades michael@0: update.setAttribute("version", this.displayVersion); michael@0: michael@0: // Optional attributes michael@0: if (this.billboardURL) michael@0: update.setAttribute("billboardURL", this.billboardURL); michael@0: if (this.detailsURL) michael@0: update.setAttribute("detailsURL", this.detailsURL); michael@0: if (this.licenseURL) michael@0: update.setAttribute("licenseURL", this.licenseURL); michael@0: if (this.platformVersion) michael@0: update.setAttribute("platformVersion", this.platformVersion); michael@0: if (this.previousAppVersion) michael@0: update.setAttribute("previousAppVersion", this.previousAppVersion); michael@0: if (this.statusText) michael@0: update.setAttribute("statusText", this.statusText); michael@0: if (this.unsupported) michael@0: update.setAttribute("unsupported", this.unsupported); michael@0: updates.documentElement.appendChild(update); michael@0: michael@0: for (var p in this._properties) { michael@0: if (this._properties[p].present) michael@0: update.setAttribute(p, this._properties[p].data); michael@0: } michael@0: michael@0: for (var i = 0; i < this.patchCount; ++i) michael@0: update.appendChild(this.getPatchAt(i).serialize(updates)); michael@0: michael@0: return update; michael@0: }, michael@0: michael@0: /** michael@0: * A hash of custom properties michael@0: */ michael@0: _properties: null, michael@0: michael@0: /** michael@0: * See nsIWritablePropertyBag.idl michael@0: */ michael@0: setProperty: function Update_setProperty(name, value) { michael@0: this._properties[name] = { data: value, present: true }; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIWritablePropertyBag.idl michael@0: */ michael@0: deleteProperty: function Update_deleteProperty(name) { michael@0: if (name in this._properties) michael@0: this._properties[name].present = false; michael@0: else michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIPropertyBag.idl michael@0: */ michael@0: get enumerator() { michael@0: var properties = []; michael@0: for (var p in this._properties) michael@0: properties.push(this._properties[p].data); michael@0: return new ArrayEnumerator(properties); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIPropertyBag.idl michael@0: */ michael@0: getProperty: function Update_getProperty(name) { michael@0: if (name in this._properties && this._properties[name].present) michael@0: return this._properties[name].data; michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdate, michael@0: Ci.nsIPropertyBag, michael@0: Ci.nsIWritablePropertyBag]) michael@0: }; michael@0: michael@0: const UpdateServiceFactory = { michael@0: _instance: null, michael@0: createInstance: function (outer, iid) { michael@0: if (outer != null) michael@0: throw Cr.NS_ERROR_NO_AGGREGATION; michael@0: return this._instance == null ? this._instance = new UpdateService() : michael@0: this._instance; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * UpdateService michael@0: * A Service for managing the discovery and installation of software updates. michael@0: * @constructor michael@0: */ michael@0: function UpdateService() { michael@0: LOG("Creating UpdateService"); michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, false); michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // PowerManagerService::SyncProfile (which is called for Reboot, PowerOff michael@0: // and Restart) sends the profile-change-net-teardown event. We can then michael@0: // pause the download in a similar manner to xpcom-shutdown. michael@0: Services.obs.addObserver(this, "profile-change-net-teardown", false); michael@0: #endif michael@0: } michael@0: michael@0: UpdateService.prototype = { michael@0: /** michael@0: * The downloader we are using to download updates. There is only ever one of michael@0: * these. michael@0: */ michael@0: _downloader: null, michael@0: michael@0: /** michael@0: * Incompatible add-on count. michael@0: */ michael@0: _incompatAddonsCount: 0, michael@0: michael@0: /** michael@0: * Whether or not the service registered the "online" observer. michael@0: */ michael@0: _registeredOnlineObserver: false, michael@0: michael@0: /** michael@0: * The current number of consecutive socket errors michael@0: */ michael@0: _consecutiveSocketErrors: 0, michael@0: michael@0: /** michael@0: * A timer used to retry socket errors michael@0: */ michael@0: _retryTimer: null, michael@0: michael@0: /** michael@0: * Whether or not a background update check was initiated by the michael@0: * application update timer notification. michael@0: */ michael@0: _isNotify: true, michael@0: michael@0: /** michael@0: * Handle Observer Service notifications michael@0: * @param subject michael@0: * The subject of the notification michael@0: * @param topic michael@0: * The notification name michael@0: * @param data michael@0: * Additional data michael@0: */ michael@0: observe: function AUS_observe(subject, topic, data) { michael@0: switch (topic) { michael@0: case "post-update-processing": michael@0: // Clean up any extant updates michael@0: this._postUpdateProcessing(); michael@0: break; michael@0: case "network:offline-status-changed": michael@0: this._offlineStatusChanged(data); michael@0: break; michael@0: case "nsPref:changed": michael@0: if (data == PREF_APP_UPDATE_LOG) { michael@0: gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); michael@0: } michael@0: break; michael@0: #ifdef MOZ_WIDGET_GONK michael@0: case "profile-change-net-teardown": // fall thru michael@0: #endif michael@0: case "xpcom-shutdown": michael@0: Services.obs.removeObserver(this, topic); michael@0: Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this); michael@0: michael@0: if (this._retryTimer) { michael@0: this._retryTimer.cancel(); michael@0: } michael@0: michael@0: this.pauseDownload(); michael@0: // Prevent leaking the downloader (bug 454964) michael@0: this._downloader = null; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The following needs to happen during the post-update-processing michael@0: * notification from nsUpdateServiceStub.js: michael@0: * 1. post update processing michael@0: * 2. resume of a download that was in progress during a previous session michael@0: * 3. start of a complete update download after the failure to apply a partial michael@0: * update michael@0: */ michael@0: michael@0: /** michael@0: * Perform post-processing on updates lingering in the updates directory michael@0: * from a previous application session - either report install failures (and michael@0: * optionally attempt to fetch a different version if appropriate) or michael@0: * notify the user of install success. michael@0: */ michael@0: _postUpdateProcessing: function AUS__postUpdateProcessing() { michael@0: // canCheckForUpdates will return false when metro-only updates are disabled michael@0: // from within metro. In that case we still want _postUpdateProcessing to michael@0: // run. gMetroUpdatesEnabled returns true on non Windows 8 platforms. michael@0: // We want _postUpdateProcessing to run so that it will update the history michael@0: // XML. Without updating the history XML, the about flyout will continue to michael@0: // have the "Restart to Apply Update" button (history xml indicates update michael@0: // is applied). michael@0: // TODO: I think this whole if-block should be removed since updates can michael@0: // always be applied via the about dialog, we should be running post update michael@0: // in those cases. michael@0: if (!this.canCheckForUpdates && gMetroUpdatesEnabled) { michael@0: LOG("UpdateService:_postUpdateProcessing - unable to check for " + michael@0: "updates... returning early"); michael@0: return; michael@0: } michael@0: michael@0: if (!this.canApplyUpdates) { michael@0: LOG("UpdateService:_postUpdateProcessing - unable to apply " + michael@0: "updates... returning early"); michael@0: // If the update is present in the update directly somehow, michael@0: // it would prevent us from notifying the user of futher updates. michael@0: cleanupActiveUpdate(); michael@0: return; michael@0: } michael@0: michael@0: var status = readStatusFile(getUpdatesDir()); michael@0: // STATE_NONE status means that the update.status file is present but a michael@0: // background download error occurred. michael@0: if (status == STATE_NONE) { michael@0: LOG("UpdateService:_postUpdateProcessing - no status, no update"); michael@0: cleanupActiveUpdate(); michael@0: return; michael@0: } michael@0: michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // This code is called very early in the boot process, before we've even michael@0: // had a chance to setup the UI so we can give feedback to the user. michael@0: // michael@0: // Since the download may be occuring over a link which has associated michael@0: // cost, we want to require user-consent before resuming the download. michael@0: // Also, applying an already downloaded update now is undesireable, michael@0: // since the phone will look dead while the update is being applied. michael@0: // Applying the update can take several minutes. Instead we wait until michael@0: // the UI is initialized so it is possible to give feedback to and get michael@0: // consent to update from the user. michael@0: if (isInterruptedUpdate(status)) { michael@0: LOG("UpdateService:_postUpdateProcessing - interrupted update detected - wait for user consent"); michael@0: return; michael@0: } michael@0: #endif michael@0: michael@0: var update = um.activeUpdate; michael@0: michael@0: if (status == STATE_DOWNLOADING) { michael@0: LOG("UpdateService:_postUpdateProcessing - patch found in downloading " + michael@0: "state"); michael@0: if (update && update.state != STATE_SUCCEEDED) { michael@0: // Resume download michael@0: var status = this.downloadUpdate(update, true); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (status == STATE_APPLYING) { michael@0: // This indicates that the background updater service is in either of the michael@0: // following two states: michael@0: // 1. It is in the process of applying an update in the background, and michael@0: // we just happen to be racing against that. michael@0: // 2. It has failed to apply an update for some reason, and we hit this michael@0: // case because the updater process has set the update status to michael@0: // applying, but has never finished. michael@0: // In order to differentiate between these two states, we look at the michael@0: // state field of the update object. If it's "pending", then we know michael@0: // that this is the first time we're hitting this case, so we switch michael@0: // that state to "applying" and we just wait and hope for the best. michael@0: // If it's "applying", we know that we've already been here once, so michael@0: // we really want to start from a clean state. michael@0: if (update && michael@0: (update.state == STATE_PENDING || update.state == STATE_PENDING_SVC)) { michael@0: LOG("UpdateService:_postUpdateProcessing - patch found in applying " + michael@0: "state for the first time"); michael@0: update.state = STATE_APPLYING; michael@0: um.saveUpdates(); michael@0: } else { // We get here even if we don't have an update object michael@0: LOG("UpdateService:_postUpdateProcessing - patch found in applying " + michael@0: "state for the second time"); michael@0: cleanupActiveUpdate(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // The update is only applied but not selected to be installed michael@0: if (status == STATE_APPLIED && update && update.isOSUpdate) { michael@0: LOG("UpdateService:_postUpdateProcessing - update staged as applied found"); michael@0: return; michael@0: } michael@0: michael@0: if (status == STATE_APPLIED_OS && update && update.isOSUpdate) { michael@0: // In gonk, we need to check for OS update status after startup, since michael@0: // the recovery partition won't write to update.status for us michael@0: var recoveryService = Cc["@mozilla.org/recovery-service;1"]. michael@0: getService(Ci.nsIRecoveryService); michael@0: michael@0: var fotaStatus = recoveryService.getFotaUpdateStatus(); michael@0: switch (fotaStatus) { michael@0: case Ci.nsIRecoveryService.FOTA_UPDATE_SUCCESS: michael@0: status = STATE_SUCCEEDED; michael@0: break; michael@0: case Ci.nsIRecoveryService.FOTA_UPDATE_FAIL: michael@0: status = STATE_FAILED + ": " + FOTA_GENERAL_ERROR; michael@0: break; michael@0: case Ci.nsIRecoveryService.FOTA_UPDATE_UNKNOWN: michael@0: default: michael@0: status = STATE_FAILED + ": " + FOTA_UNKNOWN_ERROR; michael@0: break; michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: if (!update) { michael@0: if (status != STATE_SUCCEEDED) { michael@0: LOG("UpdateService:_postUpdateProcessing - previous patch failed " + michael@0: "and no patch available"); michael@0: cleanupActiveUpdate(); michael@0: return; michael@0: } michael@0: update = new Update(null); michael@0: } michael@0: michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: michael@0: update.state = status; michael@0: this._sendStatusCodeTelemetryPing(status); michael@0: michael@0: if (status == STATE_SUCCEEDED) { michael@0: update.statusText = gUpdateBundle.GetStringFromName("installSuccess"); michael@0: michael@0: // Update the patch's metadata. michael@0: um.activeUpdate = update; michael@0: Services.prefs.setBoolPref(PREF_APP_UPDATE_POSTUPDATE, true); michael@0: prompter.showUpdateInstalled(); michael@0: michael@0: // Done with this update. Clean it up. michael@0: cleanupActiveUpdate(); michael@0: } michael@0: else { michael@0: // If we hit an error, then the error code will be included in the status michael@0: // string following a colon and a space. If we had an I/O error, then we michael@0: // assume that the patch is not invalid, and we re-stage the patch so that michael@0: // it can be attempted again the next time we restart. This will leave a michael@0: // space at the beginning of the error code when there is a failure which michael@0: // will be removed by using parseInt below. This prevents panic which has michael@0: // occurred numerous times previously (see bug 569642 comment #9 for one michael@0: // example) when testing releases due to forgetting to include the space. michael@0: var ary = status.split(":"); michael@0: update.state = ary[0]; michael@0: if (update.state == STATE_FAILED && ary[1]) { michael@0: if (handleUpdateFailure(update, ary[1])) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Something went wrong with the patch application process. michael@0: handleFallbackToCompleteUpdate(update, false); michael@0: michael@0: prompter.showUpdateError(update); michael@0: } michael@0: michael@0: // Now trash the MozUpdater folders which staged/bgupdates uses. michael@0: cleanUpMozUpdaterDirs(); michael@0: }, michael@0: michael@0: /** michael@0: * Submit a telemetry ping with the boolean value of a pref for a histogram michael@0: * michael@0: * @param pref michael@0: * The preference to report michael@0: * @param histogram michael@0: * The histogram ID to report to michael@0: */ michael@0: _sendBoolPrefTelemetryPing: function AUS__boolTelemetryPing(pref, histogram) { michael@0: try { michael@0: // The getPref is already wrapped in a try/catch but we never michael@0: // want telemetry pings breaking app update so we just put it michael@0: // inside the try to be safe. michael@0: let val = getPref("getBoolPref", pref, false); michael@0: Services.telemetry.getHistogramById(histogram).add(+val); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: #ifdef XP_WIN michael@0: /** michael@0: * Submit a telemetry ping with a boolean value which indicates if the service michael@0: * is installed. michael@0: * Also submits a telemetry ping with a boolean value which indicates if the michael@0: * service was at some point installed, but is now uninstalled. michael@0: */ michael@0: _sendServiceInstalledTelemetryPing: function AUS__svcInstallTelemetryPing() { michael@0: let installed = isServiceInstalled(); // Is the service installed? michael@0: let attempted = 0; michael@0: try { michael@0: let wrk = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, michael@0: "SOFTWARE\\Mozilla\\MaintenanceService", michael@0: wrk.ACCESS_READ | wrk.WOW64_64); michael@0: // Was the service at some point installed, but is now uninstalled? michael@0: attempted = wrk.readIntValue("Attempted"); michael@0: wrk.close(); michael@0: } catch(e) { michael@0: } michael@0: try { michael@0: let h = Services.telemetry.getHistogramById("UPDATER_SERVICE_INSTALLED"); michael@0: h.add(Number(installed)); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Cu.reportError(e); michael@0: } michael@0: try { michael@0: let h = Services.telemetry.getHistogramById("UPDATER_SERVICE_MANUALLY_UNINSTALLED"); michael@0: h.add(!installed && attempted ? 1 : 0); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: #endif michael@0: michael@0: /** michael@0: * Submit a telemetry ping with the int value of a pref for a histogram michael@0: * michael@0: * @param pref michael@0: * The preference to report michael@0: * @param histogram michael@0: * The histogram ID to report to michael@0: */ michael@0: _sendIntPrefTelemetryPing: function AUS__intTelemetryPing(pref, histogram) { michael@0: try { michael@0: // The getPref is already wrapped in a try/catch but we never michael@0: // want telemetry pings breaking app update so we just put it michael@0: // inside the try to be safe. michael@0: let val = getPref("getIntPref", pref, 0); michael@0: Services.telemetry.getHistogramById(histogram).add(val); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Submit the results of applying the update via telemetry. michael@0: * michael@0: * @param status michael@0: * The status of the update as read from the update.status file michael@0: */ michael@0: _sendStatusCodeTelemetryPing: function AUS__statusTelemetryPing(status) { michael@0: try { michael@0: let parts = status.split(":"); michael@0: if ((parts.length == 1 && status != STATE_SUCCEEDED) || michael@0: (parts.length > 1 && parts[0] != STATE_FAILED)) { michael@0: // Should also report STATE_DOWNLOAD_FAILED michael@0: return; michael@0: } michael@0: let result = 0; // 0 means success michael@0: if (parts.length > 1) { michael@0: result = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE; michael@0: } michael@0: Services.telemetry.getHistogramById("UPDATER_STATUS_CODES").add(result); michael@0: } catch(e) { michael@0: // Don't allow any exception to be propagated. michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Submit the interval in days since the last notification for this background michael@0: * update check. michael@0: */ michael@0: _sendLastNotifyIntervalPing: function AUS__notifyIntervalPing() { michael@0: if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LASTUPDATETIME)) { michael@0: let idSuffix = this._isNotify ? "NOTIFY" : "EXTERNAL"; michael@0: let lastUpdateTimeSeconds = getPref("getIntPref", michael@0: PREF_APP_UPDATE_LASTUPDATETIME, 0); michael@0: if (lastUpdateTimeSeconds) { michael@0: let currentTimeSeconds = Math.round(Date.now() / 1000); michael@0: if (lastUpdateTimeSeconds > currentTimeSeconds) { michael@0: try { michael@0: Services.telemetry. michael@0: getHistogramById("UPDATER_INVALID_LASTUPDATETIME_" + idSuffix). michael@0: add(1); michael@0: } catch(e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: else { michael@0: let intervalDays = (currentTimeSeconds - lastUpdateTimeSeconds) / michael@0: (60 * 60 * 24); michael@0: try { michael@0: Services.telemetry. michael@0: getHistogramById("UPDATER_INVALID_LASTUPDATETIME_" + idSuffix). michael@0: add(0); michael@0: Services.telemetry. michael@0: getHistogramById("UPDATER_LAST_NOTIFY_INTERVAL_DAYS_" + idSuffix). michael@0: add(intervalDays); michael@0: } catch(e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Submit the result for the background update check. michael@0: * michael@0: * @param code michael@0: * An integer value as defined by the PING_BGUC_* constants. michael@0: */ michael@0: _backgroundUpdateCheckCodePing: function AUS__backgroundUpdateCheckCodePing(code) { michael@0: try { michael@0: let idSuffix = this._isNotify ? "NOTIFY" : "EXTERNAL"; michael@0: Services.telemetry. michael@0: getHistogramById("UPDATER_BACKGROUND_CHECK_CODE_" + idSuffix).add(code); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Register an observer when the network comes online, so we can short-circuit michael@0: * the app.update.interval when there isn't connectivity michael@0: */ michael@0: _registerOnlineObserver: function AUS__registerOnlineObserver() { michael@0: if (this._registeredOnlineObserver) { michael@0: LOG("UpdateService:_registerOnlineObserver - observer already registered"); michael@0: return; michael@0: } michael@0: michael@0: LOG("UpdateService:_registerOnlineObserver - waiting for the network to " + michael@0: "be online, then forcing another check"); michael@0: michael@0: Services.obs.addObserver(this, "network:offline-status-changed", false); michael@0: this._registeredOnlineObserver = true; michael@0: }, michael@0: michael@0: /** michael@0: * Called from the network:offline-status-changed observer. michael@0: */ michael@0: _offlineStatusChanged: function AUS__offlineStatusChanged(status) { michael@0: if (status !== "online") { michael@0: return; michael@0: } michael@0: michael@0: Services.obs.removeObserver(this, "network:offline-status-changed"); michael@0: this._registeredOnlineObserver = false; michael@0: michael@0: LOG("UpdateService:_offlineStatusChanged - network is online, forcing " + michael@0: "another background check"); michael@0: michael@0: // the background checker is contained in notify michael@0: this._attemptResume(); michael@0: }, michael@0: michael@0: onCheckComplete: function AUS_onCheckComplete(request, updates, updateCount) { michael@0: this._selectAndInstallUpdate(updates); michael@0: }, michael@0: michael@0: onError: function AUS_onError(request, update) { michael@0: LOG("UpdateService:onError - error during background update. error code: " + michael@0: update.errorCode + ", status text: " + update.statusText); michael@0: michael@0: var maxErrors; michael@0: var errCount; michael@0: if (update.errorCode == NETWORK_ERROR_OFFLINE) { michael@0: // Register an online observer to try again michael@0: this._registerOnlineObserver(); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_OFFLINE); michael@0: return; michael@0: } michael@0: michael@0: if (update.errorCode == CERT_ATTR_CHECK_FAILED_NO_UPDATE || michael@0: update.errorCode == CERT_ATTR_CHECK_FAILED_HAS_UPDATE) { michael@0: errCount = getPref("getIntPref", PREF_APP_UPDATE_CERT_ERRORS, 0); michael@0: errCount++; michael@0: Services.prefs.setIntPref(PREF_APP_UPDATE_CERT_ERRORS, errCount); michael@0: maxErrors = getPref("getIntPref", PREF_APP_UPDATE_CERT_MAXERRORS, 5); michael@0: } michael@0: else { michael@0: update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES; michael@0: errCount = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDERRORS, 0); michael@0: errCount++; michael@0: Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount); michael@0: maxErrors = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, michael@0: 10); michael@0: } michael@0: michael@0: var pingCode; michael@0: if (errCount >= maxErrors) { michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateError(update); michael@0: michael@0: switch (update.errorCode) { michael@0: case CERT_ATTR_CHECK_FAILED_NO_UPDATE: michael@0: pingCode = PING_BGUC_CERT_ATTR_NO_UPDATE_NOTIFY; michael@0: break; michael@0: case CERT_ATTR_CHECK_FAILED_HAS_UPDATE: michael@0: pingCode = PING_BGUC_CERT_ATTR_WITH_UPDATE_NOTIFY; michael@0: break; michael@0: default: michael@0: pingCode = PING_BGUC_GENERAL_ERROR_NOTIFY; michael@0: } michael@0: } michael@0: else { michael@0: switch (update.errorCode) { michael@0: case CERT_ATTR_CHECK_FAILED_NO_UPDATE: michael@0: pingCode = PING_BGUC_CERT_ATTR_NO_UPDATE_SILENT; michael@0: break; michael@0: case CERT_ATTR_CHECK_FAILED_HAS_UPDATE: michael@0: pingCode = PING_BGUC_CERT_ATTR_WITH_UPDATE_SILENT; michael@0: break; michael@0: default: michael@0: pingCode = PING_BGUC_GENERAL_ERROR_SILENT; michael@0: } michael@0: } michael@0: this._backgroundUpdateCheckCodePing(pingCode); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a connection should be resumed michael@0: */ michael@0: _attemptResume: function AUS_attemptResume() { michael@0: LOG("UpdateService:_attemptResume") michael@0: // If a download is in progress, then resume it. michael@0: if (this._downloader && this._downloader._patch && michael@0: this._downloader._patch.state == STATE_DOWNLOADING && michael@0: this._downloader._update) { michael@0: LOG("UpdateService:_attemptResume - _patch.state: " + michael@0: this._downloader._patch.state); michael@0: // Make sure downloading is the state for selectPatch to work correctly michael@0: writeStatusFile(getUpdatesDir(), STATE_DOWNLOADING); michael@0: var status = this.downloadUpdate(this._downloader._update, michael@0: this._downloader.background); michael@0: LOG("UpdateService:_attemptResume - downloadUpdate status: " + status); michael@0: if (status == STATE_NONE) { michael@0: cleanupActiveUpdate(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this.backgroundChecker.checkForUpdates(this, false); michael@0: }, michael@0: michael@0: /** michael@0: * Notified when a timer fires michael@0: * @param timer michael@0: * The timer that fired michael@0: */ michael@0: notify: function AUS_notify(timer) { michael@0: // The telemetry below is specific to background notification. michael@0: this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_ENABLED, michael@0: "UPDATER_UPDATES_ENABLED"); michael@0: this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_METRO_ENABLED, michael@0: "UPDATER_UPDATES_METRO_ENABLED"); michael@0: this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_AUTO, michael@0: "UPDATER_UPDATES_AUTOMATIC"); michael@0: this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_STAGING_ENABLED, michael@0: "UPDATER_STAGE_ENABLED"); michael@0: michael@0: #ifdef XP_WIN michael@0: this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_SERVICE_ENABLED, michael@0: "UPDATER_SERVICE_ENABLED"); michael@0: this._sendIntPrefTelemetryPing(PREF_APP_UPDATE_SERVICE_ERRORS, michael@0: "UPDATER_SERVICE_ERRORS"); michael@0: this._sendServiceInstalledTelemetryPing(); michael@0: #endif michael@0: michael@0: this._checkForBackgroundUpdates(true); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() { michael@0: this._checkForBackgroundUpdates(false); michael@0: }, michael@0: michael@0: /** michael@0: * Checks for updates in the background. michael@0: * @param isNotify michael@0: * Whether or not a background update check was initiated by the michael@0: * application update timer notification. michael@0: */ michael@0: _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates(isNotify) { michael@0: this._isNotify = isNotify; michael@0: // From this point on, the telemetry reported differentiates between a call michael@0: // to notify and a call to checkForBackgroundUpdates so they are reported michael@0: // separately. michael@0: this._sendLastNotifyIntervalPing(); michael@0: michael@0: // If a download is in progress or the patch has been staged do nothing. michael@0: if (this.isDownloading) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_IS_DOWNLOADING); michael@0: return; michael@0: } michael@0: michael@0: if (this._downloader && this._downloader.patchIsStaged) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_IS_STAGED); michael@0: return; michael@0: } michael@0: michael@0: // The following checks will return early without notification in the call michael@0: // to checkForUpdates below. To simplify the background update check ping michael@0: // their values are checked here. michael@0: try { michael@0: if (!this.backgroundChecker.getUpdateURL(false)) { michael@0: let prefs = Services.prefs; michael@0: if (!prefs.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE)) { michael@0: if (!prefs.prefHasUserValue(PREF_APP_UPDATE_URL)) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_INVALID_DEFAULT_URL); michael@0: } michael@0: else { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_INVALID_CUSTOM_URL); michael@0: } michael@0: } michael@0: else { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_INVALID_OVERRIDE_URL); michael@0: } michael@0: } michael@0: else if (!gMetroUpdatesEnabled) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_METRO_DISABLED); michael@0: } michael@0: else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_PREF_DISABLED); michael@0: } michael@0: else if (!(gCanCheckForUpdates && hasUpdateMutex())) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_UNABLE_TO_CHECK); michael@0: } michael@0: else if (!this.backgroundChecker._enabled) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_DISABLED_FOR_SESSION); michael@0: } michael@0: } michael@0: catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: michael@0: this.backgroundChecker.checkForUpdates(this, false); michael@0: }, michael@0: michael@0: /** michael@0: * Determine the update from the specified updates that should be offered. michael@0: * If both valid major and minor updates are available the minor update will michael@0: * be offered. michael@0: * @param updates michael@0: * An array of available nsIUpdate items michael@0: * @return The nsIUpdate to offer. michael@0: */ michael@0: selectUpdate: function AUS_selectUpdate(updates) { michael@0: if (updates.length == 0) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_NO_UPDATE_FOUND); michael@0: return null; michael@0: } michael@0: michael@0: // The ping for unsupported is sent after the call to showPrompt. michael@0: if (updates.length == 1 && updates[0].unsupported) michael@0: return updates[0]; michael@0: michael@0: // Choose the newest of the available minor and major updates. michael@0: var majorUpdate = null; michael@0: var minorUpdate = null; michael@0: var vc = Services.vc; michael@0: var lastPingCode = PING_BGUC_NO_COMPAT_UPDATE_FOUND; michael@0: michael@0: updates.forEach(function(aUpdate) { michael@0: // Ignore updates for older versions of the application and updates for michael@0: // the same version of the application with the same build ID. michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: var compatVersion = TOR_BROWSER_VERSION; michael@0: #else michael@0: var compatVersion = Services.appinfo.version; michael@0: #endif michael@0: var rc = vc.compare(aUpdate.appVersion, compatVersion); michael@0: if (rc < 0 || ((rc == 0) && michael@0: (aUpdate.buildID == Services.appinfo.appBuildID))) { michael@0: LOG("UpdateService:selectUpdate - skipping update because the " + michael@0: "update's application version is less than the current " + michael@0: "application version"); michael@0: lastPingCode = PING_BGUC_UPDATE_PREVIOUS_VERSION; michael@0: return; michael@0: } michael@0: michael@0: // Skip the update if the user responded with "never" to this update's michael@0: // application version and the update specifies showNeverForVersion michael@0: // (see bug 350636). michael@0: let neverPrefName = PREF_APP_UPDATE_NEVER_BRANCH + aUpdate.appVersion; michael@0: if (aUpdate.showNeverForVersion && michael@0: getPref("getBoolPref", neverPrefName, false)) { michael@0: LOG("UpdateService:selectUpdate - skipping update because the " + michael@0: "preference " + neverPrefName + " is true"); michael@0: lastPingCode = PING_BGUC_UPDATE_NEVER_PREF; michael@0: return; michael@0: } michael@0: michael@0: switch (aUpdate.type) { michael@0: case "major": michael@0: if (!majorUpdate) michael@0: majorUpdate = aUpdate; michael@0: else if (vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0) michael@0: majorUpdate = aUpdate; michael@0: break; michael@0: case "minor": michael@0: if (!minorUpdate) michael@0: minorUpdate = aUpdate; michael@0: else if (vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0) michael@0: minorUpdate = aUpdate; michael@0: break; michael@0: default: michael@0: LOG("UpdateService:selectUpdate - skipping unknown update type: " + michael@0: aUpdate.type); michael@0: lastPingCode = PING_BGUC_UPDATE_INVALID_TYPE; michael@0: break; michael@0: } michael@0: }); michael@0: michael@0: var update = minorUpdate || majorUpdate; michael@0: if (!update) michael@0: this._backgroundUpdateCheckCodePing(lastPingCode); michael@0: michael@0: return update; michael@0: }, michael@0: michael@0: /** michael@0: * Reference to the currently selected update for when add-on compatibility michael@0: * is checked. michael@0: */ michael@0: _update: null, michael@0: michael@0: /** michael@0: * Determine which of the specified updates should be installed and begin the michael@0: * download/installation process or notify the user about the update. michael@0: * @param updates michael@0: * An array of available updates michael@0: */ michael@0: _selectAndInstallUpdate: function AUS__selectAndInstallUpdate(updates) { michael@0: // Return early if there's an active update. The user is already aware and michael@0: // is downloading or performed some user action to prevent notification. michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: if (um.activeUpdate) { michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // For gonk, the user isn't necessarily aware of the update, so we need michael@0: // to show the prompt to make sure. michael@0: this._showPrompt(um.activeUpdate); michael@0: #endif michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_HAS_ACTIVEUPDATE); michael@0: return; michael@0: } michael@0: michael@0: var updateEnabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); michael@0: if (!updateEnabled) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_PREF_DISABLED); michael@0: LOG("UpdateService:_selectAndInstallUpdate - not prompting because " + michael@0: "update is disabled"); michael@0: return; michael@0: } michael@0: michael@0: if (!gMetroUpdatesEnabled) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_METRO_DISABLED); michael@0: return; michael@0: } michael@0: michael@0: var update = this.selectUpdate(updates, updates.length); michael@0: if (!update) { michael@0: return; michael@0: } michael@0: michael@0: if (update.unsupported) { michael@0: LOG("UpdateService:_selectAndInstallUpdate - update not supported for " + michael@0: "this system"); michael@0: if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) { michael@0: LOG("UpdateService:_selectAndInstallUpdate - notifying that the " + michael@0: "update is not supported for this system"); michael@0: this._showPrompt(update); michael@0: } michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_UNSUPPORTED); michael@0: return; michael@0: } michael@0: michael@0: if (!(gCanApplyUpdates && hasUpdateMutex())) { michael@0: LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " + michael@0: "apply updates... prompting"); michael@0: this._showPrompt(update); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_UNABLE_TO_APPLY); michael@0: return; michael@0: } michael@0: michael@0: /** michael@0: # From this point on there are two possible outcomes: michael@0: # 1. download and install the update automatically michael@0: # 2. notify the user about the availability of an update michael@0: # michael@0: # Notes: michael@0: # a) if the app.update.auto preference is false then automatic download and michael@0: # install is disabled and the user will be notified. michael@0: # b) if the update has a showPrompt attribute the user will be notified. michael@0: # c) Mode is determined by the value of the app.update.mode preference. michael@0: # michael@0: # If the update when it is first read has an appVersion attribute the michael@0: # following behavior implemented in bug 530872 will occur: michael@0: # Mode Incompatible Add-ons Outcome michael@0: # 0 N/A Auto Install michael@0: # 1 Yes Notify michael@0: # 1 No Auto Install michael@0: # michael@0: # If the update when it is first read does not have an appVersion attribute michael@0: # the following deprecated behavior will occur: michael@0: # Update Type Mode Incompatible Add-ons Outcome michael@0: # Major all N/A Notify michael@0: # Minor 0 N/A Auto Install michael@0: # Minor 1 Yes Notify michael@0: # Minor 1 No Auto Install michael@0: */ michael@0: if (update.showPrompt) { michael@0: LOG("UpdateService:_selectAndInstallUpdate - prompting because the " + michael@0: "update snippet specified showPrompt"); michael@0: this._showPrompt(update); michael@0: if (!Services.metro || !Services.metro.immersive) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_SHOWPROMPT_SNIPPET); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) { michael@0: LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " + michael@0: "install is disabled"); michael@0: this._showPrompt(update); michael@0: if (!Services.metro || !Services.metro.immersive) { michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_SHOWPROMPT_PREF); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if (getPref("getIntPref", PREF_APP_UPDATE_MODE, 1) == 0) { michael@0: // Do not prompt regardless of add-on incompatibilities michael@0: LOG("UpdateService:_selectAndInstallUpdate - add-on compatibility " + michael@0: "check disabled by preference, just download the update"); michael@0: var status = this.downloadUpdate(update, true); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_ADDON_PREF_DISABLED); michael@0: return; michael@0: } michael@0: michael@0: // Only check add-on compatibility when the version changes. michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: var compatVersion = TOR_BROWSER_VERSION; michael@0: #else michael@0: var compatVersion = Services.appinfo.version; michael@0: #endif michael@0: if (update.appVersion && michael@0: Services.vc.compare(update.appVersion, compatVersion) != 0) { michael@0: this._update = update; michael@0: this._checkAddonCompatibility(); michael@0: } michael@0: else { michael@0: LOG("UpdateService:_selectAndInstallUpdate - add-on compatibility " + michael@0: "check not performed due to the update version being the same as " + michael@0: "the current application version, just download the update"); michael@0: var status = this.downloadUpdate(update, true); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_ADDON_SAME_APP_VER); michael@0: } michael@0: }, michael@0: michael@0: _showPrompt: function AUS__showPrompt(update) { michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateAvailable(update); michael@0: }, michael@0: michael@0: _checkAddonCompatibility: function AUS__checkAddonCompatibility() { michael@0: try { michael@0: var hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID); michael@0: } michael@0: catch (e) { } michael@0: michael@0: // Get all the installed add-ons michael@0: var self = this; michael@0: AddonManager.getAllAddons(function(addons) { michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: let compatVersion = self._update.platformVersion; michael@0: #else michael@0: let compatVersion = self._update.appVersion; michael@0: #endif michael@0: self._incompatibleAddons = []; michael@0: addons.forEach(function(addon) { michael@0: // Protect against code that overrides the add-ons manager and doesn't michael@0: // implement the isCompatibleWith or the findUpdates method. michael@0: if (!("isCompatibleWith" in addon) || !("findUpdates" in addon)) { michael@0: let errMsg = "Add-on doesn't implement either the isCompatibleWith " + michael@0: "or the findUpdates method!"; michael@0: if (addon.id) michael@0: errMsg += " Add-on ID: " + addon.id; michael@0: Cu.reportError(errMsg); michael@0: return; michael@0: } michael@0: michael@0: // If an add-on isn't appDisabled and isn't userDisabled then it is michael@0: // either active now or the user expects it to be active after the michael@0: // restart. If that is the case and the add-on is not installed by the michael@0: // application and is not compatible with the new application version michael@0: // then the user should be warned that the add-on will become michael@0: // incompatible. If an addon's type equals plugin it is skipped since michael@0: // checking plugins compatibility information isn't supported and michael@0: // getting the scope property of a plugin breaks in some environments michael@0: // (see bug 566787). The hotfix add-on is also ignored as it shouldn't michael@0: // block the user from upgrading. michael@0: try { michael@0: if (addon.type != "plugin" && addon.id != hotfixID && michael@0: !addon.appDisabled && !addon.userDisabled && michael@0: addon.scope != AddonManager.SCOPE_APPLICATION && michael@0: addon.isCompatible && michael@0: !addon.isCompatibleWith(compatVersion, michael@0: self._update.platformVersion)) michael@0: self._incompatibleAddons.push(addon); michael@0: } michael@0: catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: }); michael@0: michael@0: if (self._incompatibleAddons.length > 0) { michael@0: /** michael@0: # PREF_APP_UPDATE_INCOMPATIBLE_MODE michael@0: # Controls the mode in which we check for updates as follows. michael@0: # michael@0: # PREF_APP_UPDATE_INCOMPATIBLE_MODE != 1 michael@0: # We check for VersionInfo _and_ NewerVersion updates for the michael@0: # incompatible add-ons - i.e. if Foo 1.2 is installed and it is michael@0: # incompatible with the update, and we find Foo 2.0 which is but has michael@0: # not been installed, then we do NOT prompt because the user can michael@0: # download Foo 2.0 when they restart after the update during the add-on michael@0: # mismatch checking UI. This is the default, since it suppresses most michael@0: # prompt dialogs. michael@0: # michael@0: # PREF_APP_UPDATE_INCOMPATIBLE_MODE == 1 michael@0: # We check for VersionInfo updates for the incompatible add-ons - i.e. michael@0: # if the situation above with Foo 1.2 and available update to 2.0 michael@0: # applies, we DO show the prompt since a download operation will be michael@0: # required after the update. This is not the default and is supplied michael@0: # only as a hidden option for those that want it. michael@0: */ michael@0: self._updateCheckCount = self._incompatibleAddons.length; michael@0: LOG("UpdateService:_checkAddonCompatibility - checking for " + michael@0: "incompatible add-ons"); michael@0: michael@0: self._incompatibleAddons.forEach(function(addon) { michael@0: addon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, michael@0: compatVersion, this._update.platformVersion); michael@0: }, self); michael@0: } michael@0: else { michael@0: LOG("UpdateService:_checkAddonCompatibility - no incompatible " + michael@0: "add-ons found, just download the update"); michael@0: var status = self.downloadUpdate(self._update, true); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: self._update = null; michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_CHECK_NO_INCOMPAT); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // AddonUpdateListener michael@0: onCompatibilityUpdateAvailable: function(addon) { michael@0: // Remove the add-on from the list of add-ons that will become incompatible michael@0: // with the new version of the application. michael@0: for (var i = 0; i < this._incompatibleAddons.length; ++i) { michael@0: if (this._incompatibleAddons[i].id == addon.id) { michael@0: LOG("UpdateService:onCompatibilityUpdateAvailable - found update for " + michael@0: "add-on ID: " + addon.id); michael@0: this._incompatibleAddons.splice(i, 1); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onUpdateAvailable: function(addon, install) { michael@0: if (getPref("getIntPref", PREF_APP_UPDATE_INCOMPATIBLE_MODE, 0) == 1) michael@0: return; michael@0: michael@0: // If the new version of this add-on is blocklisted for the new application michael@0: // then it isn't a valid update and the user should still be warned that michael@0: // the add-on will become incompatible. michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"]. michael@0: getService(Ci.nsIBlocklistService); michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: let compatVersion = gUpdates.update.platformVersion; michael@0: #else michael@0: let compatVersion = gUpdates.update.appVersion; michael@0: #endif michael@0: if (bs.isAddonBlocklisted(addon, michael@0: compatVersion, michael@0: gUpdates.update.platformVersion)) michael@0: return; michael@0: michael@0: // Compatibility or new version updates mean the same thing here. michael@0: this.onCompatibilityUpdateAvailable(addon); michael@0: }, michael@0: michael@0: onUpdateFinished: function(addon) { michael@0: if (--this._updateCheckCount > 0) michael@0: return; michael@0: michael@0: if (this._incompatibleAddons.length > 0 || michael@0: !(gCanApplyUpdates && hasUpdateMutex())) { michael@0: LOG("UpdateService:onUpdateEnded - prompting because there are " + michael@0: "incompatible add-ons"); michael@0: this._showPrompt(this._update); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_ADDON_HAVE_INCOMPAT); michael@0: } michael@0: else { michael@0: LOG("UpdateService:_selectAndInstallUpdate - updates for all " + michael@0: "incompatible add-ons found, just download the update"); michael@0: var status = this.downloadUpdate(this._update, true); michael@0: if (status == STATE_NONE) michael@0: cleanupActiveUpdate(); michael@0: this._backgroundUpdateCheckCodePing(PING_BGUC_ADDON_UPDATES_FOR_INCOMPAT); michael@0: } michael@0: this._update = null; michael@0: }, michael@0: michael@0: /** michael@0: * The Checker used for background update checks. michael@0: */ michael@0: _backgroundChecker: null, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get backgroundChecker() { michael@0: if (!this._backgroundChecker) michael@0: this._backgroundChecker = new Checker(); michael@0: return this._backgroundChecker; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get canCheckForUpdates() { michael@0: return gCanCheckForUpdates && hasUpdateMutex(); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get canApplyUpdates() { michael@0: return gCanApplyUpdates && hasUpdateMutex(); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get canStageUpdates() { michael@0: return getCanStageUpdates(); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get isOtherInstanceHandlingUpdates() { michael@0: return !hasUpdateMutex(); michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: addDownloadListener: function AUS_addDownloadListener(listener) { michael@0: if (!this._downloader) { michael@0: LOG("UpdateService:addDownloadListener - no downloader!"); michael@0: return; michael@0: } michael@0: this._downloader.addDownloadListener(listener); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: removeDownloadListener: function AUS_removeDownloadListener(listener) { michael@0: if (!this._downloader) { michael@0: LOG("UpdateService:removeDownloadListener - no downloader!"); michael@0: return; michael@0: } michael@0: this._downloader.removeDownloadListener(listener); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: downloadUpdate: function AUS_downloadUpdate(update, background) { michael@0: if (!update) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: michael@0: // Don't download the update if the update's version is less than the michael@0: // current application's version or the update's version is the same as the michael@0: // application's version and the build ID is the same as the application's michael@0: // build ID. michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: var compatVersion = TOR_BROWSER_VERSION; michael@0: #else michael@0: var compatVersion = Services.appinfo.version; michael@0: #endif michael@0: if (update.appVersion && michael@0: (Services.vc.compare(update.appVersion, compatVersion) < 0 || michael@0: update.buildID && update.buildID == Services.appinfo.appBuildID && michael@0: update.appVersion == compatVersion)) { michael@0: LOG("UpdateService:downloadUpdate - canceling download of update since " + michael@0: "it is for an earlier or same application version and build ID.\n" + michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: "current Tor Browser version: " + compatVersion + "\n" + michael@0: "update Tor Browser version : " + update.appVersion + "\n" + michael@0: #else michael@0: "current application version: " + compatVersion + "\n" + michael@0: "update application version : " + update.appVersion + "\n" + michael@0: #endif michael@0: "current build ID: " + Services.appinfo.appBuildID + "\n" + michael@0: "update build ID : " + update.buildID); michael@0: cleanupActiveUpdate(); michael@0: return STATE_NONE; michael@0: } michael@0: michael@0: // If a download request is in progress vs. a download ready to resume michael@0: if (this.isDownloading) { michael@0: if (update.isCompleteUpdate == this._downloader.isCompleteUpdate && michael@0: background == this._downloader.background) { michael@0: LOG("UpdateService:downloadUpdate - no support for downloading more " + michael@0: "than one update at a time"); michael@0: return readStatusFile(getUpdatesDir()); michael@0: } michael@0: this._downloader.cancel(); michael@0: } michael@0: #ifdef MOZ_WIDGET_GONK michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: var activeUpdate = um.activeUpdate; michael@0: if (activeUpdate && michael@0: (activeUpdate.appVersion != update.appVersion || michael@0: activeUpdate.buildID != update.buildID)) { michael@0: // We have an activeUpdate (which presumably was interrupted), and are michael@0: // about start downloading a new one. Make sure we remove all traces michael@0: // of the active one (otherwise we'll start appending the new update.mar michael@0: // the the one that's been partially downloaded). michael@0: LOG("UpdateService:downloadUpdate - removing stale active update."); michael@0: cleanupActiveUpdate(); michael@0: } michael@0: #endif michael@0: // Set the previous application version prior to downloading the update. michael@0: update.previousAppVersion = compatVersion; michael@0: this._downloader = new Downloader(background, this); michael@0: return this._downloader.downloadUpdate(update); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: pauseDownload: function AUS_pauseDownload() { michael@0: if (this.isDownloading) { michael@0: this._downloader.cancel(); michael@0: } else if (this._retryTimer) { michael@0: // Download status is still consider as 'downloading' during retry. michael@0: // We need to cancel both retry and download at this stage. michael@0: this._retryTimer.cancel(); michael@0: this._retryTimer = null; michael@0: this._downloader.cancel(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: getUpdatesDirectory: getUpdatesDir, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get isDownloading() { michael@0: return this._downloader && this._downloader.isBusy; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: applyOsUpdate: function AUS_applyOsUpdate(aUpdate) { michael@0: if (!aUpdate.isOSUpdate || aUpdate.state != STATE_APPLIED) { michael@0: aUpdate.statusText = "fota-state-error"; michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: let osApplyToDir; michael@0: try { michael@0: aUpdate.QueryInterface(Ci.nsIWritablePropertyBag); michael@0: osApplyToDir = aUpdate.getProperty("osApplyToDir"); michael@0: } catch (e) {} michael@0: michael@0: if (!osApplyToDir) { michael@0: LOG("UpdateService:applyOsUpdate - Error: osApplyToDir is not defined" + michael@0: "in the nsIUpdate!"); michael@0: handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); michael@0: return; michael@0: } michael@0: michael@0: let updateFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: updateFile.initWithPath(osApplyToDir + "/update.zip"); michael@0: if (!updateFile.exists()) { michael@0: LOG("UpdateService:applyOsUpdate - Error: OS update is not found at " + michael@0: updateFile.path); michael@0: handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); michael@0: return; michael@0: } michael@0: michael@0: writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED_OS); michael@0: LOG("UpdateService:applyOsUpdate - Rebooting into recovery to apply " + michael@0: "FOTA update: " + updateFile.path); michael@0: try { michael@0: let recoveryService = Cc["@mozilla.org/recovery-service;1"] michael@0: .getService(Ci.nsIRecoveryService); michael@0: recoveryService.installFotaUpdate(updateFile.path); michael@0: } catch (e) { michael@0: LOG("UpdateService:applyOsUpdate - Error: Couldn't reboot into recovery" + michael@0: " to apply FOTA update " + updateFile.path); michael@0: writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED); michael@0: handleUpdateFailure(aUpdate, FOTA_RECOVERY_ERROR); michael@0: } michael@0: }, michael@0: michael@0: classID: UPDATESERVICE_CID, michael@0: classInfo: XPCOMUtils.generateCI({classID: UPDATESERVICE_CID, michael@0: contractID: UPDATESERVICE_CONTRACTID, michael@0: interfaces: [Ci.nsIApplicationUpdateService, michael@0: Ci.nsITimerCallback, michael@0: Ci.nsIObserver], michael@0: flags: Ci.nsIClassInfo.SINGLETON}), michael@0: michael@0: _xpcom_factory: UpdateServiceFactory, michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIApplicationUpdateService, michael@0: Ci.nsIUpdateCheckListener, michael@0: Ci.nsITimerCallback, michael@0: Ci.nsIObserver]) michael@0: }; michael@0: michael@0: /** michael@0: * A service to manage active and past updates. michael@0: * @constructor michael@0: */ michael@0: function UpdateManager() { michael@0: // Ensure the Active Update file is loaded michael@0: var updates = this._loadXMLFileIntoArray(getUpdateFile( michael@0: [FILE_UPDATE_ACTIVE])); michael@0: if (updates.length > 0) { michael@0: // Under some edgecases such as Windows system restore the active-update.xml michael@0: // will contain a pending update without the status file which will return michael@0: // STATE_NONE. To recover from this situation clean the updates dir and michael@0: // rewrite the active-update.xml file without the broken update. michael@0: if (readStatusFile(getUpdatesDir()) == STATE_NONE) { michael@0: cleanUpUpdatesDir(); michael@0: this._writeUpdatesToXMLFile([], getUpdateFile([FILE_UPDATE_ACTIVE])); michael@0: } michael@0: else michael@0: this._activeUpdate = updates[0]; michael@0: } michael@0: } michael@0: UpdateManager.prototype = { michael@0: /** michael@0: * All previously downloaded and installed updates, as an array of nsIUpdate michael@0: * objects. michael@0: */ michael@0: _updates: null, michael@0: michael@0: /** michael@0: * The current actively downloading/installing update, as a nsIUpdate object. michael@0: */ michael@0: _activeUpdate: null, michael@0: michael@0: /** michael@0: * Handle Observer Service notifications michael@0: * @param subject michael@0: * The subject of the notification michael@0: * @param topic michael@0: * The notification name michael@0: * @param data michael@0: * Additional data michael@0: */ michael@0: observe: function UM_observe(subject, topic, data) { michael@0: // Hack to be able to run and cleanup tests by reloading the update data. michael@0: if (topic == "um-reload-update-data") { michael@0: this._updates = this._loadXMLFileIntoArray(getUpdateFile( michael@0: [FILE_UPDATES_DB])); michael@0: this._activeUpdate = null; michael@0: var updates = this._loadXMLFileIntoArray(getUpdateFile( michael@0: [FILE_UPDATE_ACTIVE])); michael@0: if (updates.length > 0) michael@0: this._activeUpdate = updates[0]; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Loads an updates.xml formatted file into an array of nsIUpdate items. michael@0: * @param file michael@0: * A nsIFile for the updates.xml file michael@0: * @return The array of nsIUpdate items held in the file. michael@0: */ michael@0: _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(file) { michael@0: if (!file.exists()) { michael@0: LOG("UpdateManager:_loadXMLFileIntoArray: XML file does not exist"); michael@0: return []; michael@0: } michael@0: michael@0: var result = []; michael@0: var fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); michael@0: try { michael@0: var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: var doc = parser.parseFromStream(fileStream, "UTF-8", michael@0: fileStream.available(), "text/xml"); michael@0: michael@0: const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; michael@0: var updateCount = doc.documentElement.childNodes.length; michael@0: for (var i = 0; i < updateCount; ++i) { michael@0: var updateElement = doc.documentElement.childNodes.item(i); michael@0: if (updateElement.nodeType != ELEMENT_NODE || michael@0: updateElement.localName != "update") michael@0: continue; michael@0: michael@0: updateElement.QueryInterface(Ci.nsIDOMElement); michael@0: try { michael@0: var update = new Update(updateElement); michael@0: } catch (e) { michael@0: LOG("UpdateManager:_loadXMLFileIntoArray - invalid update"); michael@0: continue; michael@0: } michael@0: result.push(update); michael@0: } michael@0: } michael@0: catch (e) { michael@0: LOG("UpdateManager:_loadXMLFileIntoArray - error constructing update " + michael@0: "list. Exception: " + e); michael@0: } michael@0: fileStream.close(); michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Load the update manager, initializing state from state files. michael@0: */ michael@0: _ensureUpdates: function UM__ensureUpdates() { michael@0: if (!this._updates) { michael@0: this._updates = this._loadXMLFileIntoArray(getUpdateFile( michael@0: [FILE_UPDATES_DB])); michael@0: var activeUpdates = this._loadXMLFileIntoArray(getUpdateFile( michael@0: [FILE_UPDATE_ACTIVE])); michael@0: if (activeUpdates.length > 0) michael@0: this._activeUpdate = activeUpdates[0]; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: getUpdateAt: function UM_getUpdateAt(index) { michael@0: this._ensureUpdates(); michael@0: return this._updates[index]; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get updateCount() { michael@0: this._ensureUpdates(); michael@0: return this._updates.length; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: get activeUpdate() { michael@0: if (this._activeUpdate && michael@0: this._activeUpdate.channel != UpdateChannel.get()) { michael@0: LOG("UpdateManager:get activeUpdate - channel has changed, " + michael@0: "reloading default preferences to workaround bug 802022"); michael@0: // Workaround to get distribution preferences loaded (Bug 774618). This michael@0: // can be removed after bug 802022 is fixed. michael@0: let prefSvc = Services.prefs.QueryInterface(Ci.nsIObserver); michael@0: prefSvc.observe(null, "reload-default-prefs", null); michael@0: if (this._activeUpdate.channel != UpdateChannel.get()) { michael@0: // User switched channels, clear out any old active updates and remove michael@0: // partial downloads michael@0: this._activeUpdate = null; michael@0: this.saveUpdates(); michael@0: michael@0: // Destroy the updates directory, since we're done with it. michael@0: cleanUpUpdatesDir(); michael@0: } michael@0: } michael@0: return this._activeUpdate; michael@0: }, michael@0: set activeUpdate(activeUpdate) { michael@0: this._addUpdate(activeUpdate); michael@0: this._activeUpdate = activeUpdate; michael@0: if (!activeUpdate) { michael@0: // If |activeUpdate| is null, we have updated both lists - the active list michael@0: // and the history list, so we want to write both files. michael@0: this.saveUpdates(); michael@0: } michael@0: else michael@0: this._writeUpdatesToXMLFile([this._activeUpdate], michael@0: getUpdateFile([FILE_UPDATE_ACTIVE])); michael@0: return activeUpdate; michael@0: }, michael@0: michael@0: /** michael@0: * Add an update to the Updates list. If the item already exists in the list, michael@0: * replace the existing value with the new value. michael@0: * @param update michael@0: * The nsIUpdate object to add. michael@0: */ michael@0: _addUpdate: function UM__addUpdate(update) { michael@0: if (!update) michael@0: return; michael@0: this._ensureUpdates(); michael@0: if (this._updates) { michael@0: for (var i = 0; i < this._updates.length; ++i) { michael@0: if (this._updates[i] && michael@0: this._updates[i].appVersion == update.appVersion && michael@0: this._updates[i].buildID == update.buildID) { michael@0: // Replace the existing entry with the new value, updating michael@0: // all metadata. michael@0: this._updates[i] = update; michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: // Otherwise add it to the front of the list. michael@0: this._updates.unshift(update); michael@0: }, michael@0: michael@0: /** michael@0: * Serializes an array of updates to an XML file michael@0: * @param updates michael@0: * An array of nsIUpdate objects michael@0: * @param file michael@0: * The nsIFile object to serialize to michael@0: */ michael@0: _writeUpdatesToXMLFile: function UM__writeUpdatesToXMLFile(updates, file) { michael@0: var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"]. michael@0: createInstance(Ci.nsIFileOutputStream); michael@0: var modeFlags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | michael@0: FileUtils.MODE_TRUNCATE; michael@0: if (!file.exists()) michael@0: file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); michael@0: fos.init(file, modeFlags, FileUtils.PERMS_FILE, 0); michael@0: michael@0: try { michael@0: var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: const EMPTY_UPDATES_DOCUMENT = ""; michael@0: var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT, "text/xml"); michael@0: michael@0: for (var i = 0; i < updates.length; ++i) { michael@0: if (updates[i]) michael@0: doc.documentElement.appendChild(updates[i].serialize(doc)); michael@0: } michael@0: michael@0: var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]. michael@0: createInstance(Ci.nsIDOMSerializer); michael@0: serializer.serializeToStream(doc.documentElement, fos, null); michael@0: } michael@0: catch (e) { michael@0: } michael@0: michael@0: FileUtils.closeSafeFileOutputStream(fos); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: saveUpdates: function UM_saveUpdates() { michael@0: this._writeUpdatesToXMLFile([this._activeUpdate], michael@0: getUpdateFile([FILE_UPDATE_ACTIVE])); michael@0: if (this._activeUpdate) michael@0: this._addUpdate(this._activeUpdate); michael@0: michael@0: this._ensureUpdates(); michael@0: // Don't write updates that have a temporary state to the updates.xml file. michael@0: if (this._updates) { michael@0: let updates = this._updates.slice(); michael@0: for (let i = updates.length - 1; i >= 0; --i) { michael@0: let state = updates[i].state; michael@0: if (state == STATE_NONE || state == STATE_DOWNLOADING || michael@0: state == STATE_APPLIED || state == STATE_APPLIED_SVC || michael@0: state == STATE_PENDING || state == STATE_PENDING_SVC) { michael@0: updates.splice(i, 1); michael@0: } michael@0: } michael@0: michael@0: this._writeUpdatesToXMLFile(updates.slice(0, 10), michael@0: getUpdateFile([FILE_UPDATES_DB])); michael@0: } michael@0: }, michael@0: michael@0: refreshUpdateStatus: function UM_refreshUpdateStatus(update) { michael@0: var updateSucceeded = true; michael@0: var status = readStatusFile(getUpdatesDir()); michael@0: var ary = status.split(":"); michael@0: update.state = ary[0]; michael@0: if (update.state == STATE_FAILED && ary[1]) { michael@0: updateSucceeded = false; michael@0: if (!handleUpdateFailure(update, ary[1])) { michael@0: handleFallbackToCompleteUpdate(update, true); michael@0: } michael@0: } michael@0: if (update.state == STATE_APPLIED && shouldUseService()) { michael@0: writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SVC); michael@0: } michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: um.saveUpdates(); michael@0: michael@0: if (update.state != STATE_PENDING && update.state != STATE_PENDING_SVC) { michael@0: // Destroy the updates directory, since we're done with it. michael@0: // Make sure to not do this when the updater has fallen back to michael@0: // non-staged updates. michael@0: cleanUpUpdatesDir(updateSucceeded); michael@0: } michael@0: michael@0: // Send an observer notification which the update wizard uses in michael@0: // order to update its UI. michael@0: LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " + michael@0: "the update was staged. state: " + update.state + ", status: " + status); michael@0: Services.obs.notifyObservers(null, "update-staged", update.state); michael@0: michael@0: // Do this after *everything* else, since it will likely cause the app michael@0: // to shut down. michael@0: #ifdef MOZ_WIDGET_GONK michael@0: if (update.state == STATE_APPLIED) { michael@0: // Notify the user that an update has been staged and is ready for michael@0: // installation (i.e. that they should restart the application). We do michael@0: // not notify on failed update attempts. michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateDownloaded(update, true); michael@0: } else { michael@0: releaseSDCardMountLock(); michael@0: } michael@0: #else michael@0: // Only prompt when the UI isn't already open. michael@0: let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); michael@0: if (Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME) || michael@0: windowType && Services.wm.getMostRecentWindow(windowType)) { michael@0: return; michael@0: } michael@0: michael@0: if (update.state == STATE_APPLIED || update.state == STATE_APPLIED_SVC || michael@0: update.state == STATE_PENDING || update.state == STATE_PENDING_SVC) { michael@0: // Notify the user that an update has been staged and is ready for michael@0: // installation (i.e. that they should restart the application). michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateDownloaded(update, true); michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager, Ci.nsIObserver]) michael@0: }; michael@0: michael@0: /** michael@0: * Checker michael@0: * Checks for new Updates michael@0: * @constructor michael@0: */ michael@0: function Checker() { michael@0: } michael@0: Checker.prototype = { michael@0: /** michael@0: * The XMLHttpRequest object that performs the connection. michael@0: */ michael@0: _request : null, michael@0: michael@0: /** michael@0: * The nsIUpdateCheckListener callback michael@0: */ michael@0: _callback : null, michael@0: michael@0: /** michael@0: * The URL of the update service XML file to connect to that contains details michael@0: * about available updates. michael@0: */ michael@0: getUpdateURL: function UC_getUpdateURL(force) { michael@0: this._forced = force; michael@0: michael@0: // Use the override URL if specified. michael@0: var url = getPref("getCharPref", PREF_APP_UPDATE_URL_OVERRIDE, null); michael@0: michael@0: // Otherwise, construct the update URL from component parts. michael@0: if (!url) { michael@0: try { michael@0: url = Services.prefs.getDefaultBranch(null). michael@0: getCharPref(PREF_APP_UPDATE_URL); michael@0: } catch (e) { michael@0: } michael@0: } michael@0: michael@0: if (!url || url == "") { michael@0: LOG("Checker:getUpdateURL - update URL not defined"); michael@0: return null; michael@0: } michael@0: michael@0: url = url.replace(/%PRODUCT%/g, Services.appinfo.name); michael@0: #ifdef TOR_BROWSER_UPDATE michael@0: url = url.replace(/%VERSION%/g, TOR_BROWSER_VERSION); michael@0: #else michael@0: url = url.replace(/%VERSION%/g, Services.appinfo.version); michael@0: #endif michael@0: url = url.replace(/%BUILD_ID%/g, Services.appinfo.appBuildID); michael@0: url = url.replace(/%BUILD_TARGET%/g, Services.appinfo.OS + "_" + gABI); michael@0: url = url.replace(/%OS_VERSION%/g, gOSVersion); michael@0: if (/%LOCALE%/.test(url)) michael@0: url = url.replace(/%LOCALE%/g, getLocale()); michael@0: url = url.replace(/%CHANNEL%/g, UpdateChannel.get()); michael@0: url = url.replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion); michael@0: url = url.replace(/%DISTRIBUTION%/g, michael@0: getDistributionPrefValue(PREF_APP_DISTRIBUTION)); michael@0: url = url.replace(/%DISTRIBUTION_VERSION%/g, michael@0: getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION)); michael@0: url = url.replace(/%CUSTOM%/g, getPref("getCharPref", PREF_APP_UPDATE_CUSTOM, "")); michael@0: url = url.replace(/\+/g, "%2B"); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: url = url.replace(/%PRODUCT_MODEL%/g, gProductModel); michael@0: url = url.replace(/%B2G_VERSION%/g, getPref("getCharPref", PREF_APP_B2G_VERSION, null)); michael@0: #endif michael@0: michael@0: if (force) michael@0: url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1"; michael@0: michael@0: LOG("Checker:getUpdateURL - update URL: " + url); michael@0: return url; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: checkForUpdates: function UC_checkForUpdates(listener, force) { michael@0: LOG("Checker: checkForUpdates, force: " + force); michael@0: if (!listener) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: michael@0: Services.obs.notifyObservers(null, "update-check-start", null); michael@0: michael@0: var url = this.getUpdateURL(force); michael@0: if (!url || (!this.enabled && !force)) michael@0: return; michael@0: michael@0: this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. michael@0: createInstance(Ci.nsISupports); michael@0: // This is here to let unit test code override XHR michael@0: if (this._request.wrappedJSObject) { michael@0: this._request = this._request.wrappedJSObject; michael@0: } michael@0: this._request.open("GET", url, true); michael@0: var allowNonBuiltIn = !getPref("getBoolPref", michael@0: PREF_APP_UPDATE_CERT_REQUIREBUILTIN, true); michael@0: this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(allowNonBuiltIn); michael@0: // Prevent the request from reading from the cache. michael@0: this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; michael@0: // Prevent the request from writing to the cache. michael@0: this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: michael@0: this._request.overrideMimeType("text/xml"); michael@0: // The Cache-Control header is only interpreted by proxies and the michael@0: // final destination. It does not help if a resource is already michael@0: // cached locally. michael@0: this._request.setRequestHeader("Cache-Control", "no-cache"); michael@0: // HTTP/1.0 servers might not implement Cache-Control and michael@0: // might only implement Pragma: no-cache michael@0: this._request.setRequestHeader("Pragma", "no-cache"); michael@0: michael@0: var self = this; michael@0: this._request.addEventListener("error", function(event) { self.onError(event); } ,false); michael@0: this._request.addEventListener("load", function(event) { self.onLoad(event); }, false); michael@0: michael@0: LOG("Checker:checkForUpdates - sending request to: " + url); michael@0: this._request.send(null); michael@0: michael@0: this._callback = listener; michael@0: }, michael@0: michael@0: /** michael@0: * Returns an array of nsIUpdate objects discovered by the update check. michael@0: * @throws if the XML document element node name is not updates. michael@0: */ michael@0: get _updates() { michael@0: var updatesElement = this._request.responseXML.documentElement; michael@0: if (!updatesElement) { michael@0: LOG("Checker:_updates get - empty updates document?!"); michael@0: return []; michael@0: } michael@0: michael@0: if (updatesElement.nodeName != "updates") { michael@0: LOG("Checker:_updates get - unexpected node name!"); michael@0: throw new Error("Unexpected node name, expected: updates, got: " + michael@0: updatesElement.nodeName); michael@0: } michael@0: michael@0: const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; michael@0: var updates = []; michael@0: for (var i = 0; i < updatesElement.childNodes.length; ++i) { michael@0: var updateElement = updatesElement.childNodes.item(i); michael@0: if (updateElement.nodeType != ELEMENT_NODE || michael@0: updateElement.localName != "update") michael@0: continue; michael@0: michael@0: updateElement.QueryInterface(Ci.nsIDOMElement); michael@0: try { michael@0: var update = new Update(updateElement); michael@0: } catch (e) { michael@0: LOG("Checker:_updates get - invalid , ignoring..."); michael@0: continue; michael@0: } michael@0: update.serviceURL = this.getUpdateURL(this._forced); michael@0: update.channel = UpdateChannel.get(); michael@0: updates.push(update); michael@0: } michael@0: michael@0: return updates; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the status code for the XMLHttpRequest michael@0: */ michael@0: _getChannelStatus: function UC__getChannelStatus(request) { michael@0: var status = 0; michael@0: try { michael@0: status = request.status; michael@0: } michael@0: catch (e) { michael@0: } michael@0: michael@0: if (status == 0) michael@0: status = request.channel.QueryInterface(Ci.nsIRequest).status; michael@0: return status; michael@0: }, michael@0: michael@0: _isHttpStatusCode: function UC__isHttpStatusCode(status) { michael@0: return status >= 100 && status <= 599; michael@0: }, michael@0: michael@0: /** michael@0: * The XMLHttpRequest succeeded and the document was loaded. michael@0: * @param event michael@0: * The nsIDOMEvent for the load michael@0: */ michael@0: onLoad: function UC_onLoad(event) { michael@0: LOG("Checker:onLoad - request completed downloading document"); michael@0: michael@0: var prefs = Services.prefs; michael@0: var certs = null; michael@0: if (!prefs.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE) && michael@0: getPref("getBoolPref", PREF_APP_UPDATE_CERT_CHECKATTRS, true)) { michael@0: certs = gCertUtils.readCertPrefs(PREF_APP_UPDATE_CERTS_BRANCH); michael@0: } michael@0: michael@0: try { michael@0: // Analyze the resulting DOM and determine the set of updates. michael@0: var updates = this._updates; michael@0: LOG("Checker:onLoad - number of updates available: " + updates.length); michael@0: var allowNonBuiltIn = !getPref("getBoolPref", michael@0: PREF_APP_UPDATE_CERT_REQUIREBUILTIN, true); michael@0: gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs); michael@0: michael@0: if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CERT_ERRORS)) michael@0: Services.prefs.clearUserPref(PREF_APP_UPDATE_CERT_ERRORS); michael@0: michael@0: if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) michael@0: Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); michael@0: michael@0: // Tell the callback about the updates michael@0: this._callback.onCheckComplete(event.target, updates, updates.length); michael@0: } michael@0: catch (e) { michael@0: LOG("Checker:onLoad - there was a problem checking for updates. " + michael@0: "Exception: " + e); michael@0: var request = event.target; michael@0: var status = this._getChannelStatus(request); michael@0: LOG("Checker:onLoad - request.status: " + status); michael@0: var update = new Update(null); michael@0: update.errorCode = status; michael@0: update.statusText = getStatusTextFromCode(status, 404); michael@0: michael@0: if (this._isHttpStatusCode(status)) { michael@0: update.errorCode = HTTP_ERROR_OFFSET + status; michael@0: } michael@0: if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) { michael@0: update.errorCode = updates[0] ? CERT_ATTR_CHECK_FAILED_HAS_UPDATE michael@0: : CERT_ATTR_CHECK_FAILED_NO_UPDATE; michael@0: } michael@0: this._callback.onError(request, update); michael@0: } michael@0: michael@0: this._callback = null; michael@0: this._request = null; michael@0: }, michael@0: michael@0: /** michael@0: * There was an error of some kind during the XMLHttpRequest michael@0: * @param event michael@0: * The nsIDOMEvent for the error michael@0: */ michael@0: onError: function UC_onError(event) { michael@0: var request = event.target; michael@0: var status = this._getChannelStatus(request); michael@0: LOG("Checker:onError - request.status: " + status); michael@0: michael@0: // If we can't find an error string specific to this status code, michael@0: // just use the 200 message from above, which means everything michael@0: // "looks" fine but there was probably an XML error or a bogus file. michael@0: var update = new Update(null); michael@0: update.errorCode = status; michael@0: update.statusText = getStatusTextFromCode(status, 200); michael@0: michael@0: if (status == Cr.NS_ERROR_OFFLINE) { michael@0: // We use a separate constant here because nsIUpdate.errorCode is signed michael@0: update.errorCode = NETWORK_ERROR_OFFLINE; michael@0: } else if (this._isHttpStatusCode(status)) { michael@0: update.errorCode = HTTP_ERROR_OFFSET + status; michael@0: } michael@0: michael@0: this._callback.onError(request, update); michael@0: michael@0: this._request = null; michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not we are allowed to do update checking. michael@0: */ michael@0: _enabled: true, michael@0: get enabled() { michael@0: if (!gMetroUpdatesEnabled) { michael@0: return false; michael@0: } michael@0: michael@0: return getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true) && michael@0: gCanCheckForUpdates && hasUpdateMutex() && this._enabled; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: stopChecking: function UC_stopChecking(duration) { michael@0: // Always stop the current check michael@0: if (this._request) michael@0: this._request.abort(); michael@0: michael@0: switch (duration) { michael@0: case Ci.nsIUpdateChecker.CURRENT_SESSION: michael@0: this._enabled = false; michael@0: break; michael@0: case Ci.nsIUpdateChecker.ANY_CHECKS: michael@0: this._enabled = false; michael@0: Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, this._enabled); michael@0: break; michael@0: } michael@0: michael@0: this._callback = null; michael@0: }, michael@0: michael@0: classID: Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateChecker]) michael@0: }; michael@0: michael@0: /** michael@0: * Manages the download of updates michael@0: * @param background michael@0: * Whether or not this downloader is operating in background michael@0: * update mode. michael@0: * @param updateService michael@0: * The update service that created this downloader. michael@0: * @constructor michael@0: */ michael@0: function Downloader(background, updateService) { michael@0: LOG("Creating Downloader"); michael@0: this.background = background; michael@0: this.updateService = updateService; michael@0: } michael@0: Downloader.prototype = { michael@0: /** michael@0: * The nsIUpdatePatch that we are downloading michael@0: */ michael@0: _patch: null, michael@0: michael@0: /** michael@0: * The nsIUpdate that we are downloading michael@0: */ michael@0: _update: null, michael@0: michael@0: /** michael@0: * The nsIIncrementalDownload object handling the download michael@0: */ michael@0: _request: null, michael@0: michael@0: /** michael@0: * Whether or not the update being downloaded is a complete replacement of michael@0: * the user's existing installation or a patch representing the difference michael@0: * between the new version and the previous version. michael@0: */ michael@0: isCompleteUpdate: null, michael@0: michael@0: /** michael@0: * Cancels the active download. michael@0: */ michael@0: cancel: function Downloader_cancel(cancelError) { michael@0: LOG("Downloader: cancel"); michael@0: if (cancelError === undefined) { michael@0: cancelError = Cr.NS_BINDING_ABORTED; michael@0: } michael@0: if (this._request && this._request instanceof Ci.nsIRequest) { michael@0: this._request.cancel(cancelError); michael@0: } michael@0: releaseSDCardMountLock(); michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not a patch has been downloaded and staged for installation. michael@0: */ michael@0: get patchIsStaged() { michael@0: var readState = readStatusFile(getUpdatesDir()); michael@0: // Note that if we decide to download and apply new updates after another michael@0: // update has been successfully applied in the background, we need to stop michael@0: // checking for the APPLIED state here. michael@0: return readState == STATE_PENDING || readState == STATE_PENDING_SVC || michael@0: readState == STATE_APPLIED || readState == STATE_APPLIED_SVC; michael@0: }, michael@0: michael@0: /** michael@0: * Verify the downloaded file. We assume that the download is complete at michael@0: * this point. michael@0: */ michael@0: _verifyDownload: function Downloader__verifyDownload() { michael@0: LOG("Downloader:_verifyDownload called"); michael@0: if (!this._request) michael@0: return false; michael@0: michael@0: var destination = this._request.destination; michael@0: michael@0: // Ensure that the file size matches the expected file size. michael@0: if (destination.fileSize != this._patch.size) { michael@0: LOG("Downloader:_verifyDownload downloaded size != expected size."); michael@0: return false; michael@0: } michael@0: michael@0: LOG("Downloader:_verifyDownload downloaded size == expected size."); michael@0: var fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fileStream.init(destination, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); michael@0: michael@0: try { michael@0: var hash = Cc["@mozilla.org/security/hash;1"]. michael@0: createInstance(Ci.nsICryptoHash); michael@0: var hashFunction = Ci.nsICryptoHash[this._patch.hashFunction.toUpperCase()]; michael@0: if (hashFunction == undefined) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: hash.init(hashFunction); michael@0: hash.updateFromStream(fileStream, -1); michael@0: // NOTE: For now, we assume that the format of _patch.hashValue is hex michael@0: // encoded binary (such as what is typically output by programs like michael@0: // sha1sum). In the future, this may change to base64 depending on how michael@0: // we choose to compute these hashes. michael@0: digest = binaryToHex(hash.finish(false)); michael@0: } catch (e) { michael@0: LOG("Downloader:_verifyDownload - failed to compute hash of the " + michael@0: "downloaded update archive"); michael@0: digest = ""; michael@0: } michael@0: michael@0: fileStream.close(); michael@0: michael@0: if (digest == this._patch.hashValue.toLowerCase()) { michael@0: LOG("Downloader:_verifyDownload hashes match."); michael@0: return true; michael@0: } michael@0: michael@0: LOG("Downloader:_verifyDownload hashes do not match. "); michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Select the patch to use given the current state of updateDir and the given michael@0: * set of update patches. michael@0: * @param update michael@0: * A nsIUpdate object to select a patch from michael@0: * @param updateDir michael@0: * A nsIFile representing the update directory michael@0: * @return A nsIUpdatePatch object to download michael@0: */ michael@0: _selectPatch: function Downloader__selectPatch(update, updateDir) { michael@0: // Given an update to download, we will always try to download the patch michael@0: // for a partial update over the patch for a full update. michael@0: michael@0: /** michael@0: * Return the first UpdatePatch with the given type. michael@0: * @param type michael@0: * The type of the patch ("complete" or "partial") michael@0: * @return A nsIUpdatePatch object matching the type specified michael@0: */ michael@0: function getPatchOfType(type) { michael@0: for (var i = 0; i < update.patchCount; ++i) { michael@0: var patch = update.getPatchAt(i); michael@0: if (patch && patch.type == type) michael@0: return patch; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: // Look to see if any of the patches in the Update object has been michael@0: // pre-selected for download, otherwise we must figure out which one michael@0: // to select ourselves. michael@0: var selectedPatch = update.selectedPatch; michael@0: michael@0: var state = readStatusFile(updateDir); michael@0: michael@0: // If this is a patch that we know about, then select it. If it is a patch michael@0: // that we do not know about, then remove it and use our default logic. michael@0: var useComplete = false; michael@0: if (selectedPatch) { michael@0: LOG("Downloader:_selectPatch - found existing patch with state: " + michael@0: state); michael@0: switch (state) { michael@0: case STATE_DOWNLOADING: michael@0: LOG("Downloader:_selectPatch - resuming download"); michael@0: return selectedPatch; michael@0: #ifdef MOZ_WIDGET_GONK michael@0: case STATE_PENDING: michael@0: case STATE_APPLYING: michael@0: LOG("Downloader:_selectPatch - resuming interrupted apply"); michael@0: return selectedPatch; michael@0: case STATE_APPLIED: michael@0: LOG("Downloader:_selectPatch - already downloaded and staged"); michael@0: return null; michael@0: #else michael@0: case STATE_PENDING_SVC: michael@0: case STATE_PENDING: michael@0: LOG("Downloader:_selectPatch - already downloaded and staged"); michael@0: return null; michael@0: #endif michael@0: default: michael@0: // Something went wrong when we tried to apply the previous patch. michael@0: // Try the complete patch next time. michael@0: if (update && selectedPatch.type == "partial") { michael@0: useComplete = true; michael@0: } else { michael@0: // This is a pretty fatal error. Just bail. michael@0: LOG("Downloader:_selectPatch - failed to apply complete patch!"); michael@0: writeStatusFile(updateDir, STATE_NONE); michael@0: writeVersionFile(getUpdatesDir(), null); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: selectedPatch = null; michael@0: } michael@0: michael@0: // If we were not able to discover an update from a previous download, we michael@0: // select the best patch from the given set. michael@0: var partialPatch = getPatchOfType("partial"); michael@0: if (!useComplete) michael@0: selectedPatch = partialPatch; michael@0: if (!selectedPatch) { michael@0: if (partialPatch) michael@0: partialPatch.selected = false; michael@0: selectedPatch = getPatchOfType("complete"); michael@0: } michael@0: michael@0: // Restore the updateDir since we may have deleted it. michael@0: updateDir = getUpdatesDir(); michael@0: michael@0: // if update only contains a partial patch, selectedPatch == null here if michael@0: // the partial patch has been attempted and fails and we're trying to get a michael@0: // complete patch michael@0: if (selectedPatch) michael@0: selectedPatch.selected = true; michael@0: michael@0: update.isCompleteUpdate = useComplete; michael@0: michael@0: // Reset the Active Update object on the Update Manager and flush the michael@0: // Active Update DB. michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: um.activeUpdate = update; michael@0: michael@0: return selectedPatch; michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not we are currently downloading something. michael@0: */ michael@0: get isBusy() { michael@0: return this._request != null; michael@0: }, michael@0: michael@0: /** michael@0: * Get the nsIFile to use for downloading the active update's selected patch michael@0: */ michael@0: _getUpdateArchiveFile: function Downloader__getUpdateArchiveFile() { michael@0: var updateArchive; michael@0: #ifdef USE_UPDATE_ARCHIVE_DIR michael@0: try { michael@0: updateArchive = FileUtils.getDir(KEY_UPDATE_ARCHIVE_DIR, [], true); michael@0: } catch (e) { michael@0: return null; michael@0: } michael@0: #else michael@0: updateArchive = getUpdatesDir().clone(); michael@0: #endif michael@0: michael@0: updateArchive.append(FILE_UPDATE_ARCHIVE); michael@0: return updateArchive; michael@0: }, michael@0: michael@0: /** michael@0: * Download and stage the given update. michael@0: * @param update michael@0: * A nsIUpdate object to download a patch for. Cannot be null. michael@0: */ michael@0: downloadUpdate: function Downloader_downloadUpdate(update) { michael@0: LOG("UpdateService:_downloadUpdate"); michael@0: if (!update) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: michael@0: var updateDir = getUpdatesDir(); michael@0: michael@0: this._update = update; michael@0: michael@0: // This function may return null, which indicates that there are no patches michael@0: // to download. michael@0: this._patch = this._selectPatch(update, updateDir); michael@0: if (!this._patch) { michael@0: LOG("Downloader:downloadUpdate - no patch to download"); michael@0: return readStatusFile(updateDir); michael@0: } michael@0: this.isCompleteUpdate = this._patch.type == "complete"; michael@0: michael@0: var patchFile = null; michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: let status = readStatusFile(updateDir); michael@0: if (isInterruptedUpdate(status)) { michael@0: LOG("Downloader:downloadUpdate - interruptted update"); michael@0: // The update was interrupted. Try to locate the existing patch file. michael@0: // For an interrupted download, this allows a resume rather than a michael@0: // re-download. michael@0: patchFile = getFileFromUpdateLink(updateDir); michael@0: if (!patchFile) { michael@0: // No link file. We'll just assume that the update.mar is in the michael@0: // update directory. michael@0: patchFile = updateDir.clone(); michael@0: patchFile.append(FILE_UPDATE_ARCHIVE); michael@0: } michael@0: if (patchFile.exists()) { michael@0: LOG("Downloader:downloadUpdate - resuming with patchFile " + patchFile.path); michael@0: if (patchFile.fileSize == this._patch.size) { michael@0: LOG("Downloader:downloadUpdate - patchFile appears to be fully downloaded"); michael@0: // Bump the status along so that we don't try to redownload again. michael@0: status = STATE_PENDING; michael@0: } michael@0: } else { michael@0: LOG("Downloader:downloadUpdate - patchFile " + patchFile.path + michael@0: " doesn't exist - performing full download"); michael@0: // The patchfile doesn't exist, we might as well treat this like michael@0: // a new download. michael@0: patchFile = null; michael@0: } michael@0: if (patchFile && (status != STATE_DOWNLOADING)) { michael@0: // It looks like the patch was downloaded, but got interrupted while it michael@0: // was being verified or applied. So we'll fake the downloading portion. michael@0: michael@0: writeStatusFile(updateDir, STATE_PENDING); michael@0: michael@0: // Since the code expects the onStopRequest callback to happen michael@0: // asynchronously (And you have to call AUS_addDownloadListener michael@0: // after calling AUS_downloadUpdate) we need to defer this. michael@0: michael@0: this._downloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._downloadTimer.initWithCallback(function() { michael@0: this._downloadTimer = null; michael@0: // Send a fake onStopRequest. Filling in the destination allows michael@0: // _verifyDownload to work, and then the update will be applied. michael@0: this._request = {destination: patchFile}; michael@0: this.onStopRequest(this._request, null, Cr.NS_OK); michael@0: }.bind(this), 0, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: michael@0: // Returning STATE_DOWNLOADING makes UpdatePrompt think we're michael@0: // downloading. The onStopRequest that we spoofed above will make it michael@0: // look like the download finished. michael@0: return STATE_DOWNLOADING; michael@0: } michael@0: } michael@0: #endif michael@0: if (!patchFile) { michael@0: // Find a place to put the patchfile that we're going to download. michael@0: patchFile = this._getUpdateArchiveFile(); michael@0: } michael@0: if (!patchFile) { michael@0: return STATE_NONE; michael@0: } michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: if (patchFile.path.indexOf(updateDir.path) != 0) { michael@0: // The patchFile is in a directory which is different from the michael@0: // updateDir, create a link file. michael@0: writeLinkFile(updateDir, patchFile); michael@0: michael@0: if (!isInterruptedUpdate(status) && patchFile.exists()) { michael@0: // Remove stale patchFile michael@0: patchFile.remove(false); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: var uri = Services.io.newURI(this._patch.URL, null, null); michael@0: michael@0: this._request = Cc["@mozilla.org/network/incremental-download;1"]. michael@0: createInstance(Ci.nsIIncrementalDownload); michael@0: michael@0: LOG("Downloader:downloadUpdate - downloading from " + uri.spec + " to " + michael@0: patchFile.path); michael@0: var interval = this.background ? getPref("getIntPref", michael@0: PREF_APP_UPDATE_BACKGROUND_INTERVAL, michael@0: DOWNLOAD_BACKGROUND_INTERVAL) michael@0: : DOWNLOAD_FOREGROUND_INTERVAL; michael@0: this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval); michael@0: this._request.start(this, null); michael@0: michael@0: writeStatusFile(updateDir, STATE_DOWNLOADING); michael@0: this._patch.QueryInterface(Ci.nsIWritablePropertyBag); michael@0: this._patch.state = STATE_DOWNLOADING; michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: um.saveUpdates(); michael@0: return STATE_DOWNLOADING; michael@0: }, michael@0: michael@0: /** michael@0: * An array of download listeners to notify when we receive michael@0: * nsIRequestObserver or nsIProgressEventSink method calls. michael@0: */ michael@0: _listeners: [], michael@0: michael@0: /** michael@0: * Adds a listener to the download process michael@0: * @param listener michael@0: * A download listener, implementing nsIRequestObserver and michael@0: * nsIProgressEventSink michael@0: */ michael@0: addDownloadListener: function Downloader_addDownloadListener(listener) { michael@0: for (var i = 0; i < this._listeners.length; ++i) { michael@0: if (this._listeners[i] == listener) michael@0: return; michael@0: } michael@0: this._listeners.push(listener); michael@0: }, michael@0: michael@0: /** michael@0: * Removes a download listener michael@0: * @param listener michael@0: * The listener to remove. michael@0: */ michael@0: removeDownloadListener: function Downloader_removeDownloadListener(listener) { michael@0: for (var i = 0; i < this._listeners.length; ++i) { michael@0: if (this._listeners[i] == listener) { michael@0: this._listeners.splice(i, 1); michael@0: return; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When the async request begins michael@0: * @param request michael@0: * The nsIRequest object for the transfer michael@0: * @param context michael@0: * Additional data michael@0: */ michael@0: onStartRequest: function Downloader_onStartRequest(request, context) { michael@0: if (request instanceof Ci.nsIIncrementalDownload) michael@0: LOG("Downloader:onStartRequest - original URI spec: " + request.URI.spec + michael@0: ", final URI spec: " + request.finalURI.spec); michael@0: // Always set finalURL in onStartRequest since it can change. michael@0: this._patch.finalURL = request.finalURI.spec; michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: um.saveUpdates(); michael@0: michael@0: var listeners = this._listeners.concat(); michael@0: var listenerCount = listeners.length; michael@0: for (var i = 0; i < listenerCount; ++i) michael@0: listeners[i].onStartRequest(request, context); michael@0: }, michael@0: michael@0: /** michael@0: * When new data has been downloaded michael@0: * @param request michael@0: * The nsIRequest object for the transfer michael@0: * @param context michael@0: * Additional data michael@0: * @param progress michael@0: * The current number of bytes transferred michael@0: * @param maxProgress michael@0: * The total number of bytes that must be transferred michael@0: */ michael@0: onProgress: function Downloader_onProgress(request, context, progress, michael@0: maxProgress) { michael@0: LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress); michael@0: michael@0: if (progress > this._patch.size) { michael@0: LOG("Downloader:onProgress - progress: " + progress + michael@0: " is higher than patch size: " + this._patch.size); michael@0: // It's important that we use a different code than michael@0: // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference michael@0: // between a hash error and a wrong download error. michael@0: this.cancel(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: if (maxProgress != this._patch.size) { michael@0: LOG("Downloader:onProgress - maxProgress: " + maxProgress + michael@0: " is not equal to expectd patch size: " + this._patch.size); michael@0: // It's important that we use a different code than michael@0: // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference michael@0: // between a hash error and a wrong download error. michael@0: this.cancel(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: var listeners = this._listeners.concat(); michael@0: var listenerCount = listeners.length; michael@0: for (var i = 0; i < listenerCount; ++i) { michael@0: var listener = listeners[i]; michael@0: if (listener instanceof Ci.nsIProgressEventSink) michael@0: listener.onProgress(request, context, progress, maxProgress); michael@0: } michael@0: this.updateService._consecutiveSocketErrors = 0; michael@0: }, michael@0: michael@0: /** michael@0: * When we have new status text michael@0: * @param request michael@0: * The nsIRequest object for the transfer michael@0: * @param context michael@0: * Additional data michael@0: * @param status michael@0: * A status code michael@0: * @param statusText michael@0: * Human readable version of |status| michael@0: */ michael@0: onStatus: function Downloader_onStatus(request, context, status, statusText) { michael@0: LOG("Downloader:onStatus - status: " + status + ", statusText: " + michael@0: statusText); michael@0: michael@0: var listeners = this._listeners.concat(); michael@0: var listenerCount = listeners.length; michael@0: for (var i = 0; i < listenerCount; ++i) { michael@0: var listener = listeners[i]; michael@0: if (listener instanceof Ci.nsIProgressEventSink) michael@0: listener.onStatus(request, context, status, statusText); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When data transfer ceases michael@0: * @param request michael@0: * The nsIRequest object for the transfer michael@0: * @param context michael@0: * Additional data michael@0: * @param status michael@0: * Status code containing the reason for the cessation. michael@0: */ michael@0: onStopRequest: function Downloader_onStopRequest(request, context, status) { michael@0: if (request instanceof Ci.nsIIncrementalDownload) michael@0: LOG("Downloader:onStopRequest - original URI spec: " + request.URI.spec + michael@0: ", final URI spec: " + request.finalURI.spec + ", status: " + status); michael@0: michael@0: // XXX ehsan shouldShowPrompt should always be false here. michael@0: // But what happens when there is already a UI showing? michael@0: var state = this._patch.state; michael@0: var shouldShowPrompt = false; michael@0: var shouldRegisterOnlineObserver = false; michael@0: var shouldRetrySoon = false; michael@0: var deleteActiveUpdate = false; michael@0: var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_RETRY_TIMEOUT, michael@0: DEFAULT_UPDATE_RETRY_TIMEOUT); michael@0: var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_ERRORS, michael@0: DEFAULT_SOCKET_MAX_ERRORS); michael@0: LOG("Downloader:onStopRequest - status: " + status + ", " + michael@0: "current fail: " + this.updateService._consecutiveSocketErrors + ", " + michael@0: "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout); michael@0: if (Components.isSuccessCode(status)) { michael@0: if (this._verifyDownload()) { michael@0: state = shouldUseService() ? STATE_PENDING_SVC : STATE_PENDING; michael@0: if (this.background) { michael@0: shouldShowPrompt = !getCanStageUpdates(); michael@0: } michael@0: michael@0: // Tell the updater.exe we're ready to apply. michael@0: writeStatusFile(getUpdatesDir(), state); michael@0: writeVersionFile(getUpdatesDir(), this._update.appVersion); michael@0: this._update.installDate = (new Date()).getTime(); michael@0: this._update.statusText = gUpdateBundle.GetStringFromName("installPending"); michael@0: } michael@0: else { michael@0: LOG("Downloader:onStopRequest - download verification failed"); michael@0: state = STATE_DOWNLOAD_FAILED; michael@0: status = Cr.NS_ERROR_CORRUPTED_CONTENT; michael@0: michael@0: // Yes, this code is a string. michael@0: const vfCode = "verification_failed"; michael@0: var message = getStatusTextFromCode(vfCode, vfCode); michael@0: this._update.statusText = message; michael@0: michael@0: if (this._update.isCompleteUpdate || this._update.patchCount != 2) michael@0: deleteActiveUpdate = true; michael@0: michael@0: // Destroy the updates directory, since we're done with it. michael@0: cleanUpUpdatesDir(); michael@0: } michael@0: } else if (status == Cr.NS_ERROR_OFFLINE) { michael@0: // Register an online observer to try again. michael@0: // The online observer will continue the incremental download by michael@0: // calling downloadUpdate on the active update which continues michael@0: // downloading the file from where it was. michael@0: LOG("Downloader:onStopRequest - offline, register online observer: true"); michael@0: shouldRegisterOnlineObserver = true; michael@0: deleteActiveUpdate = false; michael@0: // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, and michael@0: // NS_ERROR_NET_RESET can be returned when disconnecting the internet while michael@0: // a download of a MAR is in progress. There may be others but I have not michael@0: // encountered them during testing. michael@0: } else if ((status == Cr.NS_ERROR_NET_TIMEOUT || michael@0: status == Cr.NS_ERROR_CONNECTION_REFUSED || michael@0: status == Cr.NS_ERROR_NET_RESET) && michael@0: this.updateService._consecutiveSocketErrors < maxFail) { michael@0: LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true"); michael@0: shouldRetrySoon = true; michael@0: deleteActiveUpdate = false; michael@0: } else if (status != Cr.NS_BINDING_ABORTED && michael@0: status != Cr.NS_ERROR_ABORT && michael@0: status != Cr.NS_ERROR_DOCUMENT_NOT_CACHED) { michael@0: LOG("Downloader:onStopRequest - non-verification failure"); michael@0: // Some sort of other failure, log this in the |statusText| property michael@0: state = STATE_DOWNLOAD_FAILED; michael@0: michael@0: // XXXben - if |request| (The Incremental Download) provided a means michael@0: // for accessing the http channel we could do more here. michael@0: michael@0: this._update.statusText = getStatusTextFromCode(status, michael@0: Cr.NS_BINDING_FAILED); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // bug891009: On FirefoxOS, manaully retry OTA download will reuse michael@0: // the Update object. We need to remove selected patch so that download michael@0: // can be triggered again successfully. michael@0: this._update.selectedPatch.selected = false; michael@0: #endif michael@0: michael@0: // Destroy the updates directory, since we're done with it. michael@0: cleanUpUpdatesDir(); michael@0: michael@0: deleteActiveUpdate = true; michael@0: } michael@0: LOG("Downloader:onStopRequest - setting state to: " + state); michael@0: this._patch.state = state; michael@0: var um = Cc["@mozilla.org/updates/update-manager;1"]. michael@0: getService(Ci.nsIUpdateManager); michael@0: if (deleteActiveUpdate) { michael@0: this._update.installDate = (new Date()).getTime(); michael@0: um.activeUpdate = null; michael@0: } michael@0: else { michael@0: if (um.activeUpdate) michael@0: um.activeUpdate.state = state; michael@0: } michael@0: um.saveUpdates(); michael@0: michael@0: // Only notify listeners about the stopped state if we michael@0: // aren't handling an internal retry. michael@0: if (!shouldRetrySoon && !shouldRegisterOnlineObserver) { michael@0: var listeners = this._listeners.concat(); michael@0: var listenerCount = listeners.length; michael@0: for (var i = 0; i < listenerCount; ++i) { michael@0: listeners[i].onStopRequest(request, context, status); michael@0: } michael@0: } michael@0: michael@0: this._request = null; michael@0: michael@0: if (state == STATE_DOWNLOAD_FAILED) { michael@0: var allFailed = true; michael@0: // Check if there is a complete update patch that can be downloaded. michael@0: if (!this._update.isCompleteUpdate && this._update.patchCount == 2) { michael@0: LOG("Downloader:onStopRequest - verification of patch failed, " + michael@0: "downloading complete update patch"); michael@0: this._update.isCompleteUpdate = true; michael@0: let updateStatus = this.downloadUpdate(this._update); michael@0: michael@0: if (updateStatus == STATE_NONE) { michael@0: cleanupActiveUpdate(); michael@0: } else { michael@0: allFailed = false; michael@0: } michael@0: } michael@0: michael@0: if (allFailed) { michael@0: LOG("Downloader:onStopRequest - all update patch downloads failed"); michael@0: // If the update UI is not open (e.g. the user closed the window while michael@0: // downloading) and if at any point this was a foreground download michael@0: // notify the user about the error. If the update was a background michael@0: // update there is no notification since the user won't be expecting it. michael@0: if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) { michael@0: try { michael@0: this._update.QueryInterface(Ci.nsIWritablePropertyBag); michael@0: var fgdl = this._update.getProperty("foregroundDownload"); michael@0: } michael@0: catch (e) { michael@0: } michael@0: michael@0: if (fgdl == "true") { michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateError(this._update); michael@0: } michael@0: } michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: // We always forward errors in B2G, since Gaia controls the update UI michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateError(this._update); michael@0: #endif michael@0: michael@0: // Prevent leaking the update object (bug 454964). michael@0: this._update = null; michael@0: } michael@0: // A complete download has been initiated or the failure was handled. michael@0: return; michael@0: } michael@0: michael@0: if (state == STATE_PENDING || state == STATE_PENDING_SVC) { michael@0: if (getCanStageUpdates()) { michael@0: LOG("Downloader:onStopRequest - attempting to stage update: " + michael@0: this._update.name); michael@0: michael@0: // Initiate the update in the background michael@0: try { michael@0: Cc["@mozilla.org/updates/update-processor;1"]. michael@0: createInstance(Ci.nsIUpdateProcessor). michael@0: processUpdate(this._update); michael@0: } catch (e) { michael@0: // Fail gracefully in case the application does not support the update michael@0: // processor service. michael@0: LOG("Downloader:onStopRequest - failed to stage update. Exception: " + michael@0: e); michael@0: if (this.background) { michael@0: shouldShowPrompt = true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Do this after *everything* else, since it will likely cause the app michael@0: // to shut down. michael@0: if (shouldShowPrompt) { michael@0: // Notify the user that an update has been downloaded and is ready for michael@0: // installation (i.e. that they should restart the application). We do michael@0: // not notify on failed update attempts. michael@0: var prompter = Cc["@mozilla.org/updates/update-prompt;1"]. michael@0: createInstance(Ci.nsIUpdatePrompt); michael@0: prompter.showUpdateDownloaded(this._update, true); michael@0: } michael@0: michael@0: if (shouldRegisterOnlineObserver) { michael@0: LOG("Downloader:onStopRequest - Registering online observer"); michael@0: this.updateService._registerOnlineObserver(); michael@0: } else if (shouldRetrySoon) { michael@0: LOG("Downloader:onStopRequest - Retrying soon"); michael@0: this.updateService._consecutiveSocketErrors++; michael@0: if (this.updateService._retryTimer) { michael@0: this.updateService._retryTimer.cancel(); michael@0: } michael@0: this.updateService._retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this.updateService._retryTimer.initWithCallback(function() { michael@0: this._attemptResume(); michael@0: }.bind(this.updateService), retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: } else { michael@0: // Prevent leaking the update object (bug 454964) michael@0: this._update = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIInterfaceRequestor.idl michael@0: */ michael@0: getInterface: function Downloader_getInterface(iid) { michael@0: // The network request may require proxy authentication, so provide the michael@0: // default nsIAuthPrompt if requested. michael@0: if (iid.equals(Ci.nsIAuthPrompt)) { michael@0: var prompt = Cc["@mozilla.org/network/default-auth-prompt;1"]. michael@0: createInstance(); michael@0: return prompt.QueryInterface(iid); michael@0: } michael@0: throw Cr.NS_NOINTERFACE; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, michael@0: Ci.nsIProgressEventSink, michael@0: Ci.nsIInterfaceRequestor]) michael@0: }; michael@0: michael@0: /** michael@0: * UpdatePrompt michael@0: * An object which can prompt the user with information about updates, request michael@0: * action, etc. Embedding clients can override this component with one that michael@0: * invokes a native front end. michael@0: * @constructor michael@0: */ michael@0: function UpdatePrompt() { michael@0: } michael@0: UpdatePrompt.prototype = { michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: checkForUpdates: function UP_checkForUpdates() { michael@0: if (this._getAltUpdateWindow()) michael@0: return; michael@0: michael@0: this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, michael@0: null, null); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: showUpdateAvailable: function UP_showUpdateAvailable(update) { michael@0: if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || michael@0: this._getUpdateWindow() || this._getAltUpdateWindow()) michael@0: return; michael@0: michael@0: var stringsPrefix = "updateAvailable_" + update.type + "."; michael@0: var title = gUpdateBundle.formatStringFromName(stringsPrefix + "title", michael@0: [update.name], 1); michael@0: var text = gUpdateBundle.GetStringFromName(stringsPrefix + "text"); michael@0: var imageUrl = ""; michael@0: this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, michael@0: UPDATE_WINDOW_NAME, "updatesavailable", update, michael@0: title, text, imageUrl); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) { michael@0: if (this._getAltUpdateWindow()) michael@0: return; michael@0: michael@0: if (background) { michael@0: if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) michael@0: return; michael@0: michael@0: var stringsPrefix = "updateDownloaded_" + update.type + "."; michael@0: var title = gUpdateBundle.formatStringFromName(stringsPrefix + "title", michael@0: [update.name], 1); michael@0: var text = gUpdateBundle.GetStringFromName(stringsPrefix + "text"); michael@0: var imageUrl = ""; michael@0: this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, michael@0: UPDATE_WINDOW_NAME, "finishedBackground", update, michael@0: title, text, imageUrl); michael@0: } else { michael@0: this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, michael@0: UPDATE_WINDOW_NAME, "finishedBackground", update); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: showUpdateInstalled: function UP_showUpdateInstalled() { michael@0: if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || michael@0: !getPref("getBoolPref", PREF_APP_UPDATE_SHOW_INSTALLED_UI, false) || michael@0: this._getUpdateWindow()) michael@0: return; michael@0: michael@0: var page = "installed"; michael@0: var win = this._getUpdateWindow(); michael@0: if (win) { michael@0: if (page && "setCurrentPage" in win) michael@0: win.setCurrentPage(page); michael@0: win.focus(); michael@0: } michael@0: else { michael@0: var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no"; michael@0: var arg = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString); michael@0: arg.data = page; michael@0: Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, null, openFeatures, arg); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: showUpdateError: function UP_showUpdateError(update) { michael@0: if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || michael@0: this._getAltUpdateWindow()) michael@0: return; michael@0: michael@0: // In some cases, we want to just show a simple alert dialog: michael@0: if (update.state == STATE_FAILED && michael@0: (update.errorCode == WRITE_ERROR || michael@0: update.errorCode == WRITE_ERROR_ACCESS_DENIED || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_SIGNALED || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID || michael@0: update.errorCode == WRITE_ERROR_SHARING_VIOLATION_NOPID || michael@0: update.errorCode == WRITE_ERROR_CALLBACK_APP || michael@0: update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR || michael@0: update.errorCode == FOTA_GENERAL_ERROR || michael@0: update.errorCode == FOTA_FILE_OPERATION_ERROR || michael@0: update.errorCode == FOTA_RECOVERY_ERROR || michael@0: update.errorCode == FOTA_UNKNOWN_ERROR)) { michael@0: var title = gUpdateBundle.GetStringFromName("updaterIOErrorTitle"); michael@0: var text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg", michael@0: [Services.appinfo.name, michael@0: Services.appinfo.name], 2); michael@0: Services.ww.getNewPrompter(null).alert(title, text); michael@0: return; michael@0: } michael@0: michael@0: if (update.errorCode == CERT_ATTR_CHECK_FAILED_NO_UPDATE || michael@0: update.errorCode == CERT_ATTR_CHECK_FAILED_HAS_UPDATE || michael@0: update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) { michael@0: this._showUIWhenIdle(null, URI_UPDATE_PROMPT_DIALOG, null, michael@0: UPDATE_WINDOW_NAME, null, update); michael@0: return; michael@0: } michael@0: michael@0: this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, michael@0: "errors", update); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateService.idl michael@0: */ michael@0: showUpdateHistory: function UP_showUpdateHistory(parent) { michael@0: this._showUI(parent, URI_UPDATE_HISTORY_DIALOG, "modal,dialog=yes", michael@0: "Update:History", null, null); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the update window if present. michael@0: */ michael@0: _getUpdateWindow: function UP__getUpdateWindow() { michael@0: return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME); michael@0: }, michael@0: michael@0: /** michael@0: * Returns an alternative update window if present. When a window with this michael@0: * windowtype is open the application update service won't open the normal michael@0: * application update user interface window. michael@0: */ michael@0: _getAltUpdateWindow: function UP__getAltUpdateWindow() { michael@0: let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); michael@0: if (!windowType) michael@0: return null; michael@0: return Services.wm.getMostRecentWindow(windowType); michael@0: }, michael@0: michael@0: /** michael@0: * Initiate a less obtrusive UI, starting with a non-modal notification alert michael@0: * @param parent michael@0: * A parent window, can be null michael@0: * @param uri michael@0: * The URI string of the dialog to show michael@0: * @param name michael@0: * The Window Name of the dialog to show, in case it is already open michael@0: * and can merely be focused michael@0: * @param page michael@0: * The page of the wizard to be displayed, if one is already open. michael@0: * @param update michael@0: * An update to pass to the UI in the window arguments. michael@0: * Can be null michael@0: * @param title michael@0: * The title for the notification alert. michael@0: * @param text michael@0: * The contents of the notification alert. michael@0: * @param imageUrl michael@0: * A URL identifying the image to put in the notification alert. michael@0: */ michael@0: _showUnobtrusiveUI: function UP__showUnobUI(parent, uri, features, name, page, michael@0: update, title, text, imageUrl) { michael@0: var observer = { michael@0: updatePrompt: this, michael@0: service: null, michael@0: timer: null, michael@0: notify: function () { michael@0: // the user hasn't restarted yet => prompt when idle michael@0: this.service.removeObserver(this, "quit-application"); michael@0: // If the update window is already open skip showing the UI michael@0: if (this.updatePrompt._getUpdateWindow()) michael@0: return; michael@0: this.updatePrompt._showUIWhenIdle(parent, uri, features, name, page, update); michael@0: }, michael@0: observe: function (aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "alertclickcallback": michael@0: this.updatePrompt._showUI(parent, uri, features, name, page, update); michael@0: // fall thru michael@0: case "quit-application": michael@0: if (this.timer) michael@0: this.timer.cancel(); michael@0: this.service.removeObserver(this, "quit-application"); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // bug 534090 - show the UI for update available notifications when the michael@0: // the system has been idle for at least IDLE_TIME without displaying an michael@0: // alert notification. michael@0: if (page == "updatesavailable") { michael@0: var idleService = Cc["@mozilla.org/widget/idleservice;1"]. michael@0: getService(Ci.nsIIdleService); michael@0: michael@0: const IDLE_TIME = getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60); michael@0: if (idleService.idleTime / 1000 >= IDLE_TIME) { michael@0: this._showUI(parent, uri, features, name, page, update); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: try { michael@0: var notifier = Cc["@mozilla.org/alerts-service;1"]. michael@0: getService(Ci.nsIAlertsService); michael@0: notifier.showAlertNotification(imageUrl, title, text, true, "", observer); michael@0: } michael@0: catch (e) { michael@0: // Failed to retrieve alerts service, platform unsupported michael@0: this._showUIWhenIdle(parent, uri, features, name, page, update); michael@0: return; michael@0: } michael@0: michael@0: observer.service = Services.obs; michael@0: observer.service.addObserver(observer, "quit-application", false); michael@0: michael@0: // bug 534090 - show the UI when idle for update available notifications. michael@0: if (page == "updatesavailable") { michael@0: this._showUIWhenIdle(parent, uri, features, name, page, update); michael@0: return; michael@0: } michael@0: michael@0: // Give the user x seconds to react before prompting as defined by michael@0: // promptWaitTime michael@0: observer.timer = Cc["@mozilla.org/timer;1"]. michael@0: createInstance(Ci.nsITimer); michael@0: observer.timer.initWithCallback(observer, update.promptWaitTime * 1000, michael@0: observer.timer.TYPE_ONE_SHOT); michael@0: }, michael@0: michael@0: /** michael@0: * Show the UI when the user was idle michael@0: * @param parent michael@0: * A parent window, can be null michael@0: * @param uri michael@0: * The URI string of the dialog to show michael@0: * @param name michael@0: * The Window Name of the dialog to show, in case it is already open michael@0: * and can merely be focused michael@0: * @param page michael@0: * The page of the wizard to be displayed, if one is already open. michael@0: * @param update michael@0: * An update to pass to the UI in the window arguments. michael@0: * Can be null michael@0: */ michael@0: _showUIWhenIdle: function UP__showUIWhenIdle(parent, uri, features, name, michael@0: page, update) { michael@0: var idleService = Cc["@mozilla.org/widget/idleservice;1"]. michael@0: getService(Ci.nsIIdleService); michael@0: michael@0: const IDLE_TIME = getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60); michael@0: if (idleService.idleTime / 1000 >= IDLE_TIME) { michael@0: this._showUI(parent, uri, features, name, page, update); michael@0: } else { michael@0: var observer = { michael@0: updatePrompt: this, michael@0: observe: function (aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "idle": michael@0: // If the update window is already open skip showing the UI michael@0: if (!this.updatePrompt._getUpdateWindow()) michael@0: this.updatePrompt._showUI(parent, uri, features, name, page, update); michael@0: // fall thru michael@0: case "quit-application": michael@0: idleService.removeIdleObserver(this, IDLE_TIME); michael@0: Services.obs.removeObserver(this, "quit-application"); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: idleService.addIdleObserver(observer, IDLE_TIME); michael@0: Services.obs.addObserver(observer, "quit-application", false); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Show the Update Checking UI michael@0: * @param parent michael@0: * A parent window, can be null michael@0: * @param uri michael@0: * The URI string of the dialog to show michael@0: * @param name michael@0: * The Window Name of the dialog to show, in case it is already open michael@0: * and can merely be focused michael@0: * @param page michael@0: * The page of the wizard to be displayed, if one is already open. michael@0: * @param update michael@0: * An update to pass to the UI in the window arguments. michael@0: * Can be null michael@0: */ michael@0: _showUI: function UP__showUI(parent, uri, features, name, page, update) { michael@0: var ary = null; michael@0: if (update) { michael@0: ary = Cc["@mozilla.org/supports-array;1"]. michael@0: createInstance(Ci.nsISupportsArray); michael@0: ary.AppendElement(update); michael@0: } michael@0: michael@0: var win = this._getUpdateWindow(); michael@0: if (win) { michael@0: if (page && "setCurrentPage" in win) michael@0: win.setCurrentPage(page); michael@0: win.focus(); michael@0: } michael@0: else { michael@0: var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no"; michael@0: if (features) michael@0: openFeatures += "," + features; michael@0: Services.ww.openWindow(parent, uri, "", openFeatures, ary); michael@0: } michael@0: }, michael@0: michael@0: classDescription: "Update Prompt", michael@0: contractID: "@mozilla.org/updates/update-prompt;1", michael@0: classID: Components.ID("{27ABA825-35B5-4018-9FDD-F99250A0E722}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt]) michael@0: }; michael@0: michael@0: var components = [UpdateService, Checker, UpdatePrompt, UpdateManager]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); michael@0: michael@0: #if 0 michael@0: /** michael@0: * Logs a message and stack trace to the console. michael@0: * @param string michael@0: * The string to write to the console. michael@0: */ michael@0: function STACK(string) { michael@0: dump("*** " + string + "\n"); michael@0: stackTrace(arguments.callee.caller.arguments, -1); michael@0: } michael@0: michael@0: function stackTraceFunctionFormat(aFunctionName) { michael@0: var classDelimiter = aFunctionName.indexOf("_"); michael@0: var className = aFunctionName.substr(0, classDelimiter); michael@0: if (!className) michael@0: className = ""; michael@0: var functionName = aFunctionName.substr(classDelimiter + 1, aFunctionName.length); michael@0: if (!functionName) michael@0: functionName = ""; michael@0: return className + "::" + functionName; michael@0: } michael@0: michael@0: function stackTraceArgumentsFormat(aArguments) { michael@0: arglist = ""; michael@0: for (var i = 0; i < aArguments.length; i++) { michael@0: arglist += aArguments[i]; michael@0: if (i < aArguments.length - 1) michael@0: arglist += ", "; michael@0: } michael@0: return arglist; michael@0: } michael@0: michael@0: function stackTrace(aArguments, aMaxCount) { michael@0: dump("=[STACKTRACE]=====================================================\n"); michael@0: dump("*** at: " + stackTraceFunctionFormat(aArguments.callee.name) + "(" + michael@0: stackTraceArgumentsFormat(aArguments) + ")\n"); michael@0: var temp = aArguments.callee.caller; michael@0: var count = 0; michael@0: while (temp) { michael@0: dump("*** " + stackTraceFunctionFormat(temp.name) + "(" + michael@0: stackTraceArgumentsFormat(temp.arguments) + ")\n"); michael@0: michael@0: temp = temp.arguments.callee.caller; michael@0: if (aMaxCount > 0 && ++count == aMaxCount) michael@0: break; michael@0: } michael@0: dump("==================================================================\n"); michael@0: } michael@0: michael@0: function dumpFile(file) { michael@0: dump("*** file = " + file.path + ", exists = " + file.exists() + "\n"); michael@0: } michael@0: #endif