diff options
Diffstat (limited to 'toolkit/mozapps/update/nsUpdateService.js')
-rw-r--r-- | toolkit/mozapps/update/nsUpdateService.js | 4567 |
1 files changed, 4567 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/nsUpdateService.js b/toolkit/mozapps/update/nsUpdateService.js new file mode 100644 index 000000000..10eb1d100 --- /dev/null +++ b/toolkit/mozapps/update/nsUpdateService.js @@ -0,0 +1,4567 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict'; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/FileUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/ctypes.jsm", this); +Cu.import("resource://gre/modules/UpdateTelemetry.jsm", this); +Cu.import("resource://gre/modules/AppConstants.jsm", this); +Cu.importGlobalProperties(["XMLHttpRequest"]); + +const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"); +const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1"; + +const PREF_APP_UPDATE_ALTWINDOWTYPE = "app.update.altwindowtype"; +const PREF_APP_UPDATE_AUTO = "app.update.auto"; +const PREF_APP_UPDATE_BACKGROUNDINTERVAL = "app.update.download.backgroundInterval"; +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; +const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations"; +const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; +const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; +const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version"; +const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; +const PREF_APP_UPDATE_IDLETIME = "app.update.idletime"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; +const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate"; +const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; +const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors"; +const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors"; +const PREF_APP_UPDATE_SILENT = "app.update.silent"; +const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors"; +const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout"; +const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; +const PREF_APP_UPDATE_URL = "app.update.url"; +const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; + +const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; + +const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; +const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul"; +const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update"; +const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul"; +const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties"; + +const KEY_UPDROOT = "UpdRootD"; +const KEY_EXECUTABLE = "XREExeF"; +// Gonk only +const KEY_UPDATE_ARCHIVE_DIR = "UpdArchD"; + +const DIR_UPDATES = "updates"; + +const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_UPDATES_XML = "updates.xml"; +const FILE_UPDATE_LINK = "update.link"; +const FILE_UPDATE_LOG = "update.log"; +const FILE_UPDATE_MAR = "update.mar"; +const FILE_UPDATE_STATUS = "update.status"; +const FILE_UPDATE_TEST = "update.test"; +const FILE_UPDATE_VERSION = "update.version"; + +const STATE_NONE = "null"; +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SERVICE = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_OS = "applied-os"; +const STATE_APPLIED_SERVICE = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +// The values below used by this code are from common/errors.h +const WRITE_ERROR = 7; +const ELEVATION_CANCELED = 9; +const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24; +const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25; +const SERVICE_UPDATER_SIGN_ERROR = 26; +const SERVICE_UPDATER_COMPARE_ERROR = 27; +const SERVICE_UPDATER_IDENTITY_ERROR = 28; +const SERVICE_STILL_APPLYING_ON_SUCCESS = 29; +const SERVICE_STILL_APPLYING_ON_FAILURE = 30; +const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31; +const SERVICE_COULD_NOT_LOCK_UPDATER = 32; +const SERVICE_INSTALLDIR_ERROR = 33; +const WRITE_ERROR_ACCESS_DENIED = 35; +const WRITE_ERROR_CALLBACK_APP = 37; +const FILESYSTEM_MOUNT_READWRITE_ERROR = 43; +const SERVICE_COULD_NOT_COPY_UPDATER = 49; +const SERVICE_STILL_APPLYING_TERMINATED = 50; +const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51; +const WRITE_ERROR_FILE_COPY = 61; +const WRITE_ERROR_DELETE_FILE = 62; +const WRITE_ERROR_OPEN_PATCH_FILE = 63; +const WRITE_ERROR_PATCH_FILE = 64; +const WRITE_ERROR_APPLY_DIR_PATH = 65; +const WRITE_ERROR_CALLBACK_PATH = 66; +const WRITE_ERROR_FILE_ACCESS_DENIED = 67; +const WRITE_ERROR_DIR_ACCESS_DENIED = 68; +const WRITE_ERROR_DELETE_BACKUP = 69; +const WRITE_ERROR_EXTRACT = 70; + +// Array of write errors to simplify checks for write errors +const WRITE_ERRORS = [WRITE_ERROR, + WRITE_ERROR_ACCESS_DENIED, + WRITE_ERROR_CALLBACK_APP, + WRITE_ERROR_FILE_COPY, + WRITE_ERROR_DELETE_FILE, + WRITE_ERROR_OPEN_PATCH_FILE, + WRITE_ERROR_PATCH_FILE, + WRITE_ERROR_APPLY_DIR_PATH, + WRITE_ERROR_CALLBACK_PATH, + WRITE_ERROR_FILE_ACCESS_DENIED, + WRITE_ERROR_DIR_ACCESS_DENIED, + WRITE_ERROR_DELETE_BACKUP, + WRITE_ERROR_EXTRACT]; + +// Array of write errors to simplify checks for service errors +const SERVICE_ERRORS = [SERVICE_UPDATER_COULD_NOT_BE_STARTED, + SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS, + SERVICE_UPDATER_SIGN_ERROR, + SERVICE_UPDATER_COMPARE_ERROR, + SERVICE_UPDATER_IDENTITY_ERROR, + SERVICE_STILL_APPLYING_ON_SUCCESS, + SERVICE_STILL_APPLYING_ON_FAILURE, + SERVICE_UPDATER_NOT_FIXED_DRIVE, + SERVICE_COULD_NOT_LOCK_UPDATER, + SERVICE_INSTALLDIR_ERROR, + SERVICE_COULD_NOT_COPY_UPDATER, + SERVICE_STILL_APPLYING_TERMINATED, + SERVICE_STILL_APPLYING_NO_EXIT_CODE]; + +// Error codes 80 through 99 are reserved for nsUpdateService.js and are not +// defined in common/errors.h +const FOTA_GENERAL_ERROR = 80; +const FOTA_UNKNOWN_ERROR = 81; +const FOTA_FILE_OPERATION_ERROR = 82; +const FOTA_RECOVERY_ERROR = 83; +const INVALID_UPDATER_STATE_CODE = 98; +const INVALID_UPDATER_STATUS_CODE = 99; + +// Custom update error codes +const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; +const NETWORK_ERROR_OFFLINE = 111; + +// Error codes should be < 1000. Errors above 1000 represent http status codes +const HTTP_ERROR_OFFSET = 1000; + +const DOWNLOAD_CHUNK_SIZE = 300000; // bytes +const DOWNLOAD_BACKGROUND_INTERVAL = 600; // seconds +const DOWNLOAD_FOREGROUND_INTERVAL = 0; + +const UPDATE_WINDOW_NAME = "Update:Wizard"; + +// The number of consecutive failures when updating using the service before +// setting the app.update.service.enabled preference to false. +const DEFAULT_SERVICE_MAX_ERRORS = 10; + +// The number of consecutive socket errors to allow before falling back to +// downloading a different MAR file or failing if already downloading the full. +const DEFAULT_SOCKET_MAX_ERRORS = 10; + +// The number of milliseconds to wait before retrying a connection error. +const DEFAULT_SOCKET_RETRYTIMEOUT = 2000; + +// Default maximum number of elevation cancelations per update version before +// giving up. +const DEFAULT_CANCELATIONS_OSX_MAX = 3; + +// This maps app IDs to their respective notification topic which signals when +// the application's user interface has been displayed. +const APPID_TO_TOPIC = { + // Firefox + "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored", + // SeaMonkey + "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored", + // Fennec + "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "sessionstore-windows-restored", + // Thunderbird + "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done", + // Instantbird + "{33cb9019-c295-46dd-be21-8c4936574bee}": "xul-window-visible", +}; + +var gUpdateMutexHandle = null; + +// Gonk only +var gSDCardMountLock = null; + +// Gonk only +XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + return Services.env.get("EXTERNAL_STORAGE"); +}); + +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() { + return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); +}); + +XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() { + return Services.strings.createBundle(URI_UPDATES_PROPERTIES); +}); + +// shared code for suppressing bad cert dialogs +XPCOMUtils.defineLazyGetter(this, "gCertUtils", function aus_gCertUtils() { + let temp = { }; + Cu.import("resource://gre/modules/CertUtils.jsm", temp); + return temp; +}); + +/** + * Tests to make sure that we can write to a given directory. + * + * @param updateTestFile a test file in the directory that needs to be tested. + * @param createDirectory whether a test directory should be created. + * @throws if we don't have right access to the directory. + */ +function testWriteAccess(updateTestFile, createDirectory) { + const NORMAL_FILE_TYPE = Ci.nsILocalFile.NORMAL_FILE_TYPE; + const DIRECTORY_TYPE = Ci.nsILocalFile.DIRECTORY_TYPE; + if (updateTestFile.exists()) + updateTestFile.remove(false); + updateTestFile.create(createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE, + createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE); + updateTestFile.remove(false); +} + +/** + * Windows only function that closes a Win32 handle. + * + * @param handle The handle to close + */ +function closeHandle(handle) { + let lib = ctypes.open("kernel32.dll"); + let CloseHandle = lib.declare("CloseHandle", + ctypes.winapi_abi, + ctypes.int32_t, /* success */ + ctypes.void_t.ptr); /* handle */ + CloseHandle(handle); + lib.close(); +} + +/** + * Windows only function that creates a mutex. + * + * @param aName + * The name for the mutex. + * @param aAllowExisting + * If false the function will close the handle and return null. + * @return The Win32 handle to the mutex. + */ +function createMutex(aName, aAllowExisting = true) { + if (AppConstants.platform != "win") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + + const INITIAL_OWN = 1; + const ERROR_ALREADY_EXISTS = 0xB7; + let lib = ctypes.open("kernel32.dll"); + let CreateMutexW = lib.declare("CreateMutexW", + ctypes.winapi_abi, + ctypes.void_t.ptr, /* return handle */ + ctypes.void_t.ptr, /* security attributes */ + ctypes.int32_t, /* initial owner */ + ctypes.char16_t.ptr); /* name */ + + let handle = CreateMutexW(null, INITIAL_OWN, aName); + let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS; + if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) { + closeHandle(handle); + handle = null; + } + lib.close(); + + if (handle && handle.isNull()) { + handle = null; + } + + return handle; +} + +/** + * Windows only function that determines a unique mutex name for the + * installation. + * + * @param aGlobal true if the function should return a global mutex. A global + * mutex is valid across different sessions + * @return Global mutex path + */ +function getPerInstallationMutexName(aGlobal = true) { + if (AppConstants.platform != "win") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsILocalFile); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var data = converter.convertToByteArray(exeFile.path.toLowerCase()); + + hasher.update(data, data.length); + return (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true); +} + +/** + * Whether or not the current instance has the update mutex. The update mutex + * gives protection against 2 applications from the same installation updating: + * 1) Running multiple profiles from the same installation path + * 2) Two applications running in 2 different user sessions from the same path + * + * @return true if this instance holds the update mutex + */ +function hasUpdateMutex() { + if (AppConstants.platform != "win") { + return true; + } + if (!gUpdateMutexHandle) { + gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false); + } + return !!gUpdateMutexHandle; +} + +/** + * Determines whether or not all descendants of a directory are writeable. + * Note: Does not check the root directory itself for writeability. + * + * @return true if all descendants are writeable, false otherwise + */ +function areDirectoryEntriesWriteable(aDir) { + let items = aDir.directoryEntries; + while (items.hasMoreElements()) { + let item = items.getNext().QueryInterface(Ci.nsIFile); + if (!item.isWritable()) { + LOG("areDirectoryEntriesWriteable - unable to write to " + item.path); + return false; + } + if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) { + return false; + } + } + return true; +} + +/** + * OSX only function to determine if the user requires elevation to be able to + * write to the application bundle. + * + * @return true if elevation is required, false otherwise + */ +function getElevationRequired() { + if (AppConstants.platform != "macosx") { + return false; + } + + try { + // Recursively check that the application bundle (and its descendants) can + // be written to. + LOG("getElevationRequired - recursively testing write access on " + + getInstallDirRoot().path); + if (!getInstallDirRoot().isWritable() || + !areDirectoryEntriesWriteable(getInstallDirRoot())) { + LOG("getElevationRequired - unable to write to application bundle, " + + "elevation required"); + return true; + } + } catch (ex) { + LOG("getElevationRequired - unable to write to application bundle, " + + "elevation required. Exception: " + ex); + return true; + } + LOG("getElevationRequired - able to write to application bundle, elevation " + + "not required"); + return false; +} + +/** + * Determines whether or not an update can be applied. This is always true on + * Windows when the service is used. Also, this is always true on OSX because we + * offer users the option to perform an elevated update when necessary. + * + * @return true if an update can be applied, false otherwise + */ +function getCanApplyUpdates() { + let useService = false; + if (shouldUseService()) { + // No need to perform directory write checks, the maintenance service will + // be able to write to all directories. + LOG("getCanApplyUpdates - bypass the write checks because we'll use the service"); + useService = true; + } + + if (!useService && AppConstants.platform != "macosx") { + try { + let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + LOG("getCanApplyUpdates - testing write access " + updateTestFile.path); + testWriteAccess(updateTestFile, false); + if (AppConstants.platform == "win") { + // Example windowsVersion: Windows XP == 5.1 + let windowsVersion = Services.sysinfo.getProperty("version"); + LOG("getCanApplyUpdates - windowsVersion = " + windowsVersion); + + /** + * For Vista, updates can be performed to a location requiring admin + * privileges by requesting elevation via the UAC prompt when launching + * updater.exe if the appDir is under the Program Files directory + * (e.g. C:\Program Files\) and UAC is turned on and we can elevate + * (e.g. user has a split token). + * + * Note: this does note attempt to handle the case where UAC is turned on + * and the installation directory is in a restricted location that + * requires admin privileges to update other than Program Files. + */ + let userCanElevate = false; + + if (parseFloat(windowsVersion) >= 6) { + try { + // KEY_UPDROOT will fail and throw an exception if + // appDir is not under the Program Files, so we rely on that + let dir = Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile); + // appDir is under Program Files, so check if the user can elevate + userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper). + userCanElevate; + LOG("getCanApplyUpdates - on Vista, userCanElevate: " + userCanElevate); + } + catch (ex) { + // When the installation directory is not under Program Files, + // fall through to checking if write access to the + // installation directory is available. + LOG("getCanApplyUpdates - on Vista, appDir is not under Program Files"); + } + } + + /** + * On Windows, we no longer store the update under the app dir. + * + * If we are on Windows (including Vista, if we can't elevate) we need to + * to check that we can create and remove files from the actual app + * directory (like C:\Program Files\Mozilla Firefox). If we can't + * (because this user is not an adminstrator, for example) canUpdate() + * should return false. + * + * For Vista, we perform this check to enable updating the application + * when the user has write access to the installation directory under the + * following scenarios: + * 1) the installation directory is not under Program Files + * (e.g. C:\Program Files) + * 2) UAC is turned off + * 3) UAC is turned on and the user is not an admin + * (e.g. the user does not have a split token) + * 4) UAC is turned on and the user is already elevated, so they can't be + * elevated again + */ + if (!userCanElevate) { + // if we're unable to create the test file this will throw an exception. + let appDirTestFile = getAppBaseDir(); + appDirTestFile.append(FILE_UPDATE_TEST); + LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path); + if (appDirTestFile.exists()) { + appDirTestFile.remove(false); + } + appDirTestFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + appDirTestFile.remove(false); + } + } + } catch (e) { + LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e); + // No write privileges to install directory + return false; + } + } // if (!useService) + + LOG("getCanApplyUpdates - able to apply updates"); + return true; +} + +/** + * Whether or not the application can stage an update for the current session. + * These checks are only performed once per session due to using a lazy getter. + * + * @return true if updates can be staged for this session. + */ +XPCOMUtils.defineLazyGetter(this, "gCanStageUpdatesSession", function aus_gCSUS() { + if (getElevationRequired()) { + LOG("gCanStageUpdatesSession - unable to stage updates because elevation " + + "is required."); + return false; + } + + try { + let updateTestFile; + if (AppConstants.platform == "macosx") { + updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + } else { + updateTestFile = getInstallDirRoot(); + updateTestFile.append(FILE_UPDATE_TEST); + } + LOG("gCanStageUpdatesSession - testing write access " + + updateTestFile.path); + testWriteAccess(updateTestFile, true); + if (AppConstants.platform != "macosx") { + // On all platforms except Mac, we need to test the parent directory as + // well, as we need to be able to move files in that directory during the + // replacing step. + updateTestFile = getInstallDirRoot().parent; + updateTestFile.append(FILE_UPDATE_TEST); + LOG("gCanStageUpdatesSession - testing write access " + + updateTestFile.path); + updateTestFile.createUnique(Ci.nsILocalFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + updateTestFile.remove(false); + } + } catch (e) { + LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + + e); + // No write privileges + return false; + } + + LOG("gCanStageUpdatesSession - able to stage updates"); + return true; +}); + +/** + * Whether or not the application can stage an update. + * + * @return true if updates can be staged. + */ +function getCanStageUpdates() { + // If staging updates are disabled, then just bail out! + if (!getPref("getBoolPref", PREF_APP_UPDATE_STAGING_ENABLED, false)) { + LOG("getCanStageUpdates - staging updates is disabled by preference " + + PREF_APP_UPDATE_STAGING_ENABLED); + return false; + } + + if (AppConstants.platform == "win" && shouldUseService()) { + // No need to perform directory write checks, the maintenance service will + // be able to write to all directories. + LOG("getCanStageUpdates - able to stage updates using the service"); + return true; + } + + // For Gonk, the updater will remount the /system partition to move staged + // files into place. + if (AppConstants.platform == "gonk") { + LOG("getCanStageUpdates - able to stage updates because this is gonk"); + return true; + } + + if (!hasUpdateMutex()) { + LOG("getCanStageUpdates - unable to apply updates because another " + + "instance of the application is already handling updates for this " + + "installation."); + return false; + } + + return gCanStageUpdatesSession; +} + +XPCOMUtils.defineLazyGetter(this, "gCanCheckForUpdates", function aus_gCanCheckForUpdates() { + // If the administrator has disabled app update and locked the preference so + // users can't check for updates. This preference check is ok in this lazy + // getter since locked prefs don't change until the application is restarted. + var enabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); + if (!enabled && Services.prefs.prefIsLocked(PREF_APP_UPDATE_ENABLED)) { + LOG("gCanCheckForUpdates - unable to automatically check for updates, " + + "the preference is disabled and admistratively locked."); + return false; + } + + // If we don't know the binary platform we're updating, we can't update. + if (!UpdateUtils.ABI) { + LOG("gCanCheckForUpdates - unable to check for updates, unknown ABI"); + return false; + } + + // If we don't know the OS version we're updating, we can't update. + if (!UpdateUtils.OSVersion) { + LOG("gCanCheckForUpdates - unable to check for updates, unknown OS " + + "version"); + return false; + } + + LOG("gCanCheckForUpdates - able to check for updates"); + return true; +}); + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (gLogEnabled) { + dump("*** AUS:SVC " + string + "\n"); + Services.console.logStringMessage("AUS:SVC " + string); + } +} + +/** + * Gets a preference value, handling the case where there is no default. + * @param func + * The name of the preference function to call, on nsIPrefBranch + * @param preference + * The name of the preference + * @param defaultValue + * The default value to return in the event the preference has + * no setting + * @return The value of the preference, or undefined if there was no + * user or default value. + */ +function getPref(func, preference, defaultValue) { + try { + return Services.prefs[func](preference); + } + catch (e) { + } + return defaultValue; +} + +/** + * Convert a string containing binary values to hex. + */ +function binaryToHex(input) { + var result = ""; + for (var i = 0; i < input.length; ++i) { + var hex = input.charCodeAt(i).toString(16); + if (hex.length == 1) + hex = "0" + hex; + result += hex; + } + return result; +} + +/** + * Gets the specified directory at the specified hierarchy under the + * update root directory and creates it if it doesn't exist. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key| + * @return nsIFile object for the location specified. + */ +function getUpdateDirCreate(pathArray) { + return FileUtils.getDir(KEY_UPDROOT, pathArray, true); +} + +/** + * Gets the application base directory. + * + * @return nsIFile object for the application base directory. + */ +function getAppBaseDir() { + return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent; +} + +/** + * Gets the root of the installation directory which is the application + * bundle directory on Mac OS X and the location of the application binary + * on all other platforms. + * + * @return nsIFile object for the directory + */ +function getInstallDirRoot() { + let dir = getAppBaseDir(); + if (AppConstants.platform == "macosx") { + // On Mac, we store the Updated.app directory inside the bundle directory. + dir = dir.parent.parent; + } + return dir; +} + +/** + * Gets the file at the specified hierarchy under the update root directory. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key|. The last item in this array must be the + * leaf name of a file. + * @return nsIFile object for the file specified. The file is NOT created + * if it does not exist, however all required directories along + * the way are. + */ +function getUpdateFile(pathArray) { + let file = getUpdateDirCreate(pathArray.slice(0, -1)); + file.append(pathArray[pathArray.length - 1]); + return file; +} + +/** + * Returns human readable status text from the updates.properties bundle + * based on an error code + * @param code + * The error code to look up human readable status text for + * @param defaultCode + * The default code to look up should human readable status text + * not exist for |code| + * @return A human readable status text string + */ +function getStatusTextFromCode(code, defaultCode) { + let reason; + try { + reason = gUpdateBundle.GetStringFromName("check_error-" + code); + LOG("getStatusTextFromCode - transfer error: " + reason + ", code: " + + code); + } + catch (e) { + // Use the default reason + reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode); + LOG("getStatusTextFromCode - transfer error: " + reason + + ", default code: " + defaultCode); + } + return reason; +} + +/** + * Get the Active Updates directory + * @return The active updates directory, as a nsIFile object + */ +function getUpdatesDir() { + // Right now, we only support downloading one patch at a time, so we always + // use the same target directory. + return getUpdateDirCreate([DIR_UPDATES, "0"]); +} + +/** + * Reads the update state from the update.status file in the specified + * directory. + * @param dir + * The dir to look for an update.status file in + * @return The status value of the update. + */ +function readStatusFile(dir) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + let status = readStringFromFile(statusFile) || STATE_NONE; + LOG("readStatusFile - status: " + status + ", path: " + statusFile.path); + return status; +} + +/** + * Writes the current update operation/state to a file in the patch + * directory, indicating to the patching system that operations need + * to be performed. + * @param dir + * The patch directory where the update.status file should be + * written. + * @param state + * The state value to write. + */ +function writeStatusFile(dir, state) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + writeStringToFile(statusFile, state); +} + +/** + * Writes the update's application version to a file in the patch directory. If + * the update doesn't provide application version information via the + * appVersion attribute the string "null" will be written to the file. + * This value is compared during startup (in nsUpdateDriver.cpp) to determine if + * the update should be applied. Note that this won't provide protection from + * downgrade of the application for the nightly user case where the application + * version doesn't change. + * @param dir + * The patch directory where the update.version file should be + * written. + * @param version + * The version value to write. Will be the string "null" when the + * update doesn't provide the appVersion attribute in the update xml. + */ +function writeVersionFile(dir, version) { + let versionFile = dir.clone(); + versionFile.append(FILE_UPDATE_VERSION); + writeStringToFile(versionFile, version); +} + +/** + * Gonk only function that reads the link file specified in the update.link file + * in the specified directory and returns the nsIFile for the corresponding + * file. + * @param dir + * The dir to look for an update.link file in + * @return A nsIFile for the file path specified in the + * update.link file or null if the update.link file + * doesn't exist. + */ +function getFileFromUpdateLink(dir) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let linkFile = dir.clone(); + linkFile.append(FILE_UPDATE_LINK); + let link = readStringFromFile(linkFile); + LOG("getFileFromUpdateLink linkFile.path: " + linkFile.path + ", link: " + link); + if (!link) { + return null; + } + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(link); + return file; +} + +/** + * Gonk only function to create a link file. This allows the actual patch to + * live in a directory different from the update directory. + * @param dir + * The patch directory where the update.link file + * should be written. + * @param patchFile + * The fully qualified filename of the patchfile. + */ +function writeLinkFile(dir, patchFile) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let linkFile = dir.clone(); + linkFile.append(FILE_UPDATE_LINK); + writeStringToFile(linkFile, patchFile.path); + if (patchFile.path.indexOf(gExtStorage) == 0) { + // The patchfile is being stored on external storage. Try to lock it + // so that it doesn't get shared with the PC while we're downloading + // to it. + acquireSDCardMountLock(); + } +} + +/** + * Gonk only function to acquire a VolumeMountLock for the sdcard volume. + * + * This prevents the SDCard from being shared with the PC while + * we're downloading the update. + */ +function acquireSDCardMountLock() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let volsvc = Cc["@mozilla.org/telephony/volume-service;1"]. + getService(Ci.nsIVolumeService); + if (volsvc) { + gSDCardMountLock = volsvc.createMountLock("sdcard"); + } +} + +/** + * Gonk only function that determines if the state corresponds to an + * interrupted update. This could either be because the download was + * interrupted, or because staging the update was interrupted. + * + * @return true if the state corresponds to an interrupted + * update. + */ +function isInterruptedUpdate(status) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + return (status == STATE_DOWNLOADING) || + (status == STATE_PENDING) || + (status == STATE_APPLYING); +} + +/** + * Releases any SDCard mount lock that we might have. + * + * This once again allows the SDCard to be shared with the PC. + */ +function releaseSDCardMountLock() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_UNEXPECTED; + } + if (gSDCardMountLock) { + gSDCardMountLock.unlock(); + gSDCardMountLock = null; + } +} + +/** + * Determines if the service should be used to attempt an update + * or not. + * + * @return true if the service should be used for updates. + */ +function shouldUseService() { + // This function will return true if the mantenance service should be used if + // all of the following conditions are met: + // 1) This build was done with the maintenance service enabled + // 2) The maintenance service is installed + // 3) The pref for using the service is enabled + // 4) The Windows version is XP Service Pack 3 or above (for SHA-2 support) + // The maintenance service requires SHA-2 support because we sign our binaries + // with a SHA-2 certificate and the certificate is verified before the binary + // is launched. + if (!AppConstants.MOZ_MAINTENANCE_SERVICE || !isServiceInstalled() || + !getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false) || + !AppConstants.isPlatformAndVersionAtLeast("win", "5.1") /* WinXP */) { + return false; + } + + // If it's newer than XP, then the service pack doesn't matter. + if (Services.sysinfo.getProperty("version") != "5.1") { + return true; + } + + // If the Windows version is XP, we also need to check the service pack. + // We'll return false if only < SP3 is installed, or if we can't tell. + // Check the service pack level by calling GetVersionEx via ctypes. + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + let kernel32 = false; + try { + kernel32 = ctypes.open("Kernel32"); + } catch (e) { + Cu.reportError("Unable to open kernel32! " + e); + return false; + } + + if (kernel32) { + try { + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if (0 !== GetVersionEx(winVer.address())) { + return winVer.wServicePackMajor >= 3; + } + Cu.reportError("Unknown failure in GetVersionEX (returned 0)"); + return false; + } catch (e) { + Cu.reportError("Error getting service pack information. Exception: " + e); + return false; + } + } finally { + kernel32.close(); + } + } + + // If the service pack check couldn't be done, assume we can't use the service. + return false; +} + +/** + * Determines if the service is is installed. + * + * @return true if the service is installed. + */ +function isServiceInstalled() { + if (AppConstants.MOZ_MAINTENANCE_SERVICE && AppConstants.platform == "win") { + let installed = 0; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) { + } + installed = installed == 1; // convert to bool + LOG("isServiceInstalled = " + installed); + return installed; + } + return false; +} + +/** + * Removes the contents of the updates patch directory and rotates the update + * logs when present. If the update.log exists in the patch directory this will + * move the last-update.log if it exists to backup-update.log in the parent + * directory of the patch directory and then move the update.log in the patch + * directory to last-update.log in the parent directory of the patch directory. + * + * @param aRemovePatchFiles (optional, defaults to true) + * When true the update's patch directory contents are removed. + */ +function cleanUpUpdatesDir(aRemovePatchFiles = true) { + let updateDir; + try { + updateDir = getUpdatesDir(); + } catch (e) { + LOG("cleanUpUpdatesDir - unable to get the updates patch directory. " + + "Exception: " + e); + return; + } + + // Preserve the last update log file for debugging purposes. + let updateLogFile = updateDir.clone(); + updateLogFile.append(FILE_UPDATE_LOG); + if (updateLogFile.exists()) { + let dir = updateDir.parent; + let logFile = dir.clone(); + logFile.append(FILE_LAST_UPDATE_LOG); + if (logFile.exists()) { + try { + logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to rename file " + logFile.path + + " to " + FILE_BACKUP_UPDATE_LOG); + } + } + + try { + updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to rename file " + updateLogFile.path + + " to " + FILE_LAST_UPDATE_LOG); + } + } + + if (aRemovePatchFiles) { + let dirEntries = updateDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let file = dirEntries.getNext().QueryInterface(Ci.nsIFile); + if (AppConstants.platform == "gonk") { + if (file.leafName == FILE_UPDATE_LINK) { + let linkedFile = getFileFromUpdateLink(updateDir); + if (linkedFile && linkedFile.exists()) { + linkedFile.remove(false); + } + } + } + + // Now, recursively remove this file. The recursive removal is needed for + // Mac OSX because this directory will contain a copy of updater.app, + // which is itself a directory and the MozUpdater directory on platforms + // other than Windows. + try { + file.remove(true); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to remove file " + file.path); + } + } + } + if (AppConstants.platform == "gonk") { + releaseSDCardMountLock(); + } +} + +/** + * Clean up updates list and the updates directory. + */ +function cleanupActiveUpdate() { + // Move the update from the Active Update list into the Past Updates list. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate = null; + um.saveUpdates(); + + // Now trash the updates directory, since we're done with it + cleanUpUpdatesDir(); +} + +/** + * An enumeration of items in a JS array. + * @constructor + */ +function ArrayEnumerator(aItems) { + this._index = 0; + if (aItems) { + for (var i = 0; i < aItems.length; ++i) { + if (!aItems[i]) + aItems.splice(i, 1); + } + } + this._contents = aItems; +} + +ArrayEnumerator.prototype = { + _index: 0, + _contents: [], + + hasMoreElements: function ArrayEnumerator_hasMoreElements() { + return this._index < this._contents.length; + }, + + getNext: function ArrayEnumerator_getNext() { + return this._contents[this._index++]; + } +}; + +/** + * Writes a string of text to a file. A newline will be appended to the data + * written to the file. This function only works with ASCII text. + */ +function writeStringToFile(file, text) { + let fos = FileUtils.openSafeFileOutputStream(file); + text += "\n"; + fos.write(text, text.length); + FileUtils.closeSafeFileOutputStream(fos); +} + +function readStringFromInputStream(inputStream) { + var sis = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + sis.init(inputStream); + var text = sis.read(sis.available()); + sis.close(); + if (text && text[text.length - 1] == "\n") { + text = text.slice(0, -1); + } + return text; +} + +/** + * Reads a string of text from a file. A trailing newline will be removed + * before the result is returned. This function only works with ASCII text. + */ +function readStringFromFile(file) { + if (!file.exists()) { + LOG("readStringFromFile - file doesn't exist: " + file.path); + return null; + } + var fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + return readStringFromInputStream(fis); +} + +function handleUpdateFailure(update, errorCode) { + update.errorCode = parseInt(errorCode); + if (update.errorCode == FOTA_GENERAL_ERROR || + update.errorCode == FOTA_FILE_OPERATION_ERROR || + update.errorCode == FOTA_RECOVERY_ERROR || + update.errorCode == FOTA_UNKNOWN_ERROR) { + // In the case of FOTA update errors, don't reset the state to pending. This + // causes the FOTA update path to try again, which is not necessarily what + // we want. + update.statusText = gUpdateBundle.GetStringFromName("statusFailed"); + + Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt). + showUpdateError(update); + writeStatusFile(getUpdatesDir(), STATE_FAILED + ": " + errorCode); + cleanupActiveUpdate(); + return true; + } + + // Replace with Array.prototype.includes when it has stabilized. + if (WRITE_ERRORS.indexOf(update.errorCode) != -1 || + update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR) { + Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt). + showUpdateError(update); + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + return true; + } + + if (update.errorCode == ELEVATION_CANCELED) { + let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0); + cancelations++; + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations); + if (AppConstants.platform == "macosx") { + let osxCancelations = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX, 0); + osxCancelations++; + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX, + osxCancelations); + let maxCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX); + // Prevent the preference from setting a value greater than 5. + maxCancels = Math.min(maxCancels, 5); + if (osxCancelations >= maxCancels) { + cleanupActiveUpdate(); + } else { + writeStatusFile(getUpdatesDir(), + update.state = STATE_PENDING_ELEVATE); + } + update.statusText = gUpdateBundle.GetStringFromName("elevationFailure"); + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("patchingFailed", "elevationFailure"); + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + } else { + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + } + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + + // Replace with Array.prototype.includes when it has stabilized. + if (SERVICE_ERRORS.indexOf(update.errorCode) != -1) { + var failCount = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_ERRORS, 0); + var maxFail = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_MAXERRORS, + DEFAULT_SERVICE_MAX_ERRORS); + // Prevent the preference from setting a value greater than 10. + maxFail = Math.min(maxFail, 10); + // As a safety, when the service reaches maximum failures, it will + // disable itself and fallback to using the normal update mechanism + // without the service. + if (failCount >= maxFail) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } else { + failCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, + failCount); + } + + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } + + return false; +} + +/** + * Fall back to downloading a complete update in case an update has failed. + * + * @param update the update object that has failed to apply. + * @param postStaging true if we have just attempted to stage an update. + */ +function handleFallbackToCompleteUpdate(update, postStaging) { + cleanupActiveUpdate(); + + update.statusText = gUpdateBundle.GetStringFromName("patchApplyFailure"); + var oldType = update.selectedPatch ? update.selectedPatch.type + : "complete"; + if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) { + // Partial patch application failed, try downloading the complete + // update in the background instead. + LOG("handleFallbackToCompleteUpdate - install of partial patch " + + "failed, downloading complete patch"); + var status = Cc["@mozilla.org/updates/update-service;1"]. + getService(Ci.nsIApplicationUpdateService). + downloadUpdate(update, !postStaging); + if (status == STATE_NONE) + cleanupActiveUpdate(); + } + else { + LOG("handleFallbackToCompleteUpdate - install of complete or " + + "only one patch offered failed."); + } + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("patchingFailed", oldType); +} + +function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) { + let patchType = AUSTLMY.PATCH_UNKNOWN; + if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) { + if (aUpdate.selectedPatch.type == "complete") { + patchType = AUSTLMY.PATCH_COMPLETE; + } else if (aUpdate.selectedPatch.type == "partial") { + patchType = AUSTLMY.PATCH_PARTIAL; + } + } + + let suffix = patchType + "_" + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE); + let stateCode = 0; + let parts = aStatus.split(":"); + if (parts.length > 0) { + switch (parts[0]) { + case STATE_NONE: + stateCode = 2; + break; + case STATE_DOWNLOADING: + stateCode = 3; + break; + case STATE_PENDING: + stateCode = 4; + break; + case STATE_PENDING_SERVICE: + stateCode = 5; + break; + case STATE_APPLYING: + stateCode = 6; + break; + case STATE_APPLIED: + stateCode = 7; + break; + case STATE_APPLIED_OS: + stateCode = 8; + break; + case STATE_APPLIED_SERVICE: + stateCode = 9; + break; + case STATE_SUCCEEDED: + stateCode = 10; + break; + case STATE_DOWNLOAD_FAILED: + stateCode = 11; + break; + case STATE_FAILED: + stateCode = 12; + break; + case STATE_PENDING_ELEVATE: + stateCode = 13; + break; + default: + stateCode = 1; + } + + if (parts.length > 1) { + let statusErrorCode = INVALID_UPDATER_STATE_CODE; + if (parts[0] == STATE_FAILED) { + statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE; + } + AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode); + } + } + AUSTLMY.pingStateCode(suffix, stateCode); +} + +/** + * Update Patch + * @param patch + * A <patch> element to initialize this object with + * @throws if patch has a size of 0 + * @constructor + */ +function UpdatePatch(patch) { + this._properties = {}; + for (var i = 0; i < patch.attributes.length; ++i) { + var attr = patch.attributes.item(i); + attr.QueryInterface(Ci.nsIDOMAttr); + switch (attr.name) { + case "selected": + this.selected = attr.value == "true"; + break; + case "size": + if (0 == parseInt(attr.value)) { + LOG("UpdatePatch:init - 0-sized patch!"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + // fall through + default: + this[attr.name] = attr.value; + break; + } + } +} +UpdatePatch.prototype = { + /** + * See nsIUpdateService.idl + */ + serialize: function UpdatePatch_serialize(updates) { + var patch = updates.createElementNS(URI_UPDATE_NS, "patch"); + patch.setAttribute("type", this.type); + patch.setAttribute("URL", this.URL); + // finalURL is not available until after the download has started + if (this.finalURL) { + patch.setAttribute("finalURL", this.finalURL); + } + patch.setAttribute("hashFunction", this.hashFunction); + patch.setAttribute("hashValue", this.hashValue); + patch.setAttribute("size", this.size); + if (this.selected) { + patch.setAttribute("selected", this.selected); + } + patch.setAttribute("state", this.state); + + for (let p in this._properties) { + if (this._properties[p].present) { + patch.setAttribute(p, this._properties[p].data); + } + } + + return patch; + }, + + /** + * A hash of custom properties + */ + _properties: null, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function UpdatePatch_setProperty(name, value) { + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function UpdatePatch_deleteProperty(name) { + if (name in this._properties) + this._properties[name].present = false; + else + throw Cr.NS_ERROR_FAILURE; + }, + + /** + * See nsIPropertyBag.idl + */ + get enumerator() { + var properties = []; + for (var p in this._properties) + properties.push(this._properties[p].data); + return new ArrayEnumerator(properties); + }, + + /** + * See nsIPropertyBag.idl + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function UpdatePatch_getProperty(name) { + if (name in this._properties && + this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + /** + * Returns whether or not the update.status file for this patch exists at the + * appropriate location. + */ + get statusFileExists() { + var statusFile = getUpdatesDir(); + statusFile.append(FILE_UPDATE_STATUS); + return statusFile.exists(); + }, + + /** + * See nsIUpdateService.idl + */ + get state() { + if (this._properties.state) + return this._properties.state; + return STATE_NONE; + }, + set state(val) { + this._properties.state = val; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePatch, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag]) +}; + +/** + * Update + * Implements nsIUpdate + * @param update + * An <update> element to initialize this object with + * @throws if the update contains no patches + * @constructor + */ +function Update(update) { + this._properties = {}; + this._patches = []; + this.isCompleteUpdate = false; + this.isOSUpdate = false; + this.showPrompt = false; + this.showNeverForVersion = false; + this.unsupported = false; + this.channel = "default"; + this.promptWaitTime = getPref("getIntPref", PREF_APP_UPDATE_PROMPTWAITTIME, 43200); + this.backgroundInterval = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDINTERVAL, + DOWNLOAD_BACKGROUND_INTERVAL); + + // Null <update>, assume this is a message container and do no + // further initialization + if (!update) { + return; + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + let patch; + for (let i = 0; i < update.childNodes.length; ++i) { + let patchElement = update.childNodes.item(i); + if (patchElement.nodeType != ELEMENT_NODE || + patchElement.localName != "patch") { + continue; + } + + patchElement.QueryInterface(Ci.nsIDOMElement); + try { + patch = new UpdatePatch(patchElement); + } catch (e) { + continue; + } + this._patches.push(patch); + } + + if (this._patches.length == 0 && !update.hasAttribute("unsupported")) { + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + + // Set the installDate value with the current time. If the update has an + // installDate attribute this will be replaced with that value if it doesn't + // equal 0. + this.installDate = (new Date()).getTime(); + + for (let i = 0; i < update.attributes.length; ++i) { + var attr = update.attributes.item(i); + attr.QueryInterface(Ci.nsIDOMAttr); + if (attr.value == "undefined") { + continue; + } else if (attr.name == "detailsURL") { + this._detailsURL = attr.value; + } else if (attr.name == "installDate" && attr.value) { + let val = parseInt(attr.value); + if (val) { + this.installDate = val; + } + } else if (attr.name == "isCompleteUpdate") { + this.isCompleteUpdate = attr.value == "true"; + } else if (attr.name == "isSecurityUpdate") { + this.isSecurityUpdate = attr.value == "true"; + } else if (attr.name == "isOSUpdate") { + this.isOSUpdate = attr.value == "true"; + } else if (attr.name == "showNeverForVersion") { + this.showNeverForVersion = attr.value == "true"; + } else if (attr.name == "showPrompt") { + this.showPrompt = attr.value == "true"; + } else if (attr.name == "promptWaitTime") { + if (!isNaN(attr.value)) { + this.promptWaitTime = parseInt(attr.value); + } + } else if (attr.name == "backgroundInterval") { + if (!isNaN(attr.value)) { + this.backgroundInterval = parseInt(attr.value); + } + } else if (attr.name == "unsupported") { + this.unsupported = attr.value == "true"; + } else { + this[attr.name] = attr.value; + + switch (attr.name) { + case "appVersion": + case "buildID": + case "channel": + case "displayVersion": + case "name": + case "previousAppVersion": + case "serviceURL": + case "statusText": + case "type": + break; + default: + // Save custom attributes when serializing to the local xml file but + // don't use this method for the expected attributes which are already + // handled in serialize. + this.setProperty(attr.name, attr.value); + break; + } + } + } + + if (!this.displayVersion) { + this.displayVersion = this.appVersion; + } + + // Don't allow the background download interval to be greater than 10 minutes. + this.backgroundInterval = Math.min(this.backgroundInterval, 600); + + // The Update Name is either the string provided by the <update> element, or + // the string: "<App Name> <Update App Version>" + var name = ""; + if (update.hasAttribute("name")) { + name = update.getAttribute("name"); + } else { + var brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); + var appName = brandBundle.GetStringFromName("brandShortName"); + name = gUpdateBundle.formatStringFromName("updateName", + [appName, this.displayVersion], 2); + } + this.name = name; +} +Update.prototype = { + /** + * See nsIUpdateService.idl + */ + get patchCount() { + return this._patches.length; + }, + + /** + * See nsIUpdateService.idl + */ + getPatchAt: function Update_getPatchAt(index) { + return this._patches[index]; + }, + + /** + * See nsIUpdateService.idl + * + * We use a copy of the state cached on this object in |_state| only when + * there is no selected patch, i.e. in the case when we could not load + * |.activeUpdate| from the update manager for some reason but still have + * the update.status file to work with. + */ + _state: "", + set state(state) { + if (this.selectedPatch) + this.selectedPatch.state = state; + this._state = state; + return state; + }, + get state() { + if (this.selectedPatch) + return this.selectedPatch.state; + return this._state; + }, + + /** + * See nsIUpdateService.idl + */ + errorCode: 0, + + /** + * See nsIUpdateService.idl + */ + get selectedPatch() { + for (var i = 0; i < this.patchCount; ++i) { + if (this._patches[i].selected) + return this._patches[i]; + } + return null; + }, + + /** + * See nsIUpdateService.idl + */ + get detailsURL() { + if (!this._detailsURL) { + try { + // Try using a default details URL supplied by the distribution + // if the update XML does not supply one. + return Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS); + } + catch (e) { + } + } + return this._detailsURL || ""; + }, + + /** + * See nsIUpdateService.idl + */ + serialize: function Update_serialize(updates) { + // If appVersion isn't defined just return null. This happens when cleaning + // up invalid updates (e.g. incorrect channel). + if (!this.appVersion) { + return null; + } + var update = updates.createElementNS(URI_UPDATE_NS, "update"); + update.setAttribute("appVersion", this.appVersion); + update.setAttribute("buildID", this.buildID); + update.setAttribute("channel", this.channel); + update.setAttribute("displayVersion", this.displayVersion); + update.setAttribute("installDate", this.installDate); + update.setAttribute("isCompleteUpdate", this.isCompleteUpdate); + update.setAttribute("isOSUpdate", this.isOSUpdate); + update.setAttribute("name", this.name); + update.setAttribute("serviceURL", this.serviceURL); + update.setAttribute("showNeverForVersion", this.showNeverForVersion); + update.setAttribute("showPrompt", this.showPrompt); + update.setAttribute("promptWaitTime", this.promptWaitTime); + update.setAttribute("backgroundInterval", this.backgroundInterval); + update.setAttribute("type", this.type); + + if (this.detailsURL) { + update.setAttribute("detailsURL", this.detailsURL); + } + if (this.previousAppVersion) { + update.setAttribute("previousAppVersion", this.previousAppVersion); + } + if (this.statusText) { + update.setAttribute("statusText", this.statusText); + } + if (this.unsupported) { + update.setAttribute("unsupported", this.unsupported); + } + updates.documentElement.appendChild(update); + + for (let p in this._properties) { + if (this._properties[p].present) { + update.setAttribute(p, this._properties[p].data); + } + } + + for (let i = 0; i < this.patchCount; ++i) { + update.appendChild(this.getPatchAt(i).serialize(updates)); + } + + return update; + }, + + /** + * A hash of custom properties + */ + _properties: null, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function Update_setProperty(name, value) { + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function Update_deleteProperty(name) { + if (name in this._properties) + this._properties[name].present = false; + else + throw Cr.NS_ERROR_FAILURE; + }, + + /** + * See nsIPropertyBag.idl + */ + get enumerator() { + var properties = []; + for (let p in this._properties) { + properties.push(this._properties[p].data); + } + return new ArrayEnumerator(properties); + }, + + /** + * See nsIPropertyBag.idl + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function Update_getProperty(name) { + if (name in this._properties && this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdate, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag]) +}; + +const UpdateServiceFactory = { + _instance: null, + createInstance: function (outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this._instance == null ? this._instance = new UpdateService() : + this._instance; + } +}; + +/** + * UpdateService + * A Service for managing the discovery and installation of software updates. + * @constructor + */ +function UpdateService() { + LOG("Creating UpdateService"); + Services.obs.addObserver(this, "xpcom-shutdown", false); + Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, false); + if (AppConstants.platform == "gonk") { + // PowerManagerService::SyncProfile (which is called for Reboot, PowerOff + // and Restart) sends the profile-change-net-teardown event. We can then + // pause the download in a similar manner to xpcom-shutdown. + Services.obs.addObserver(this, "profile-change-net-teardown", false); + } +} + +UpdateService.prototype = { + /** + * The downloader we are using to download updates. There is only ever one of + * these. + */ + _downloader: null, + + /** + * Whether or not the service registered the "online" observer. + */ + _registeredOnlineObserver: false, + + /** + * The current number of consecutive socket errors + */ + _consecutiveSocketErrors: 0, + + /** + * A timer used to retry socket errors + */ + _retryTimer: null, + + /** + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _isNotify: true, + + /** + * Handle Observer Service notifications + * @param subject + * The subject of the notification + * @param topic + * The notification name + * @param data + * Additional data + */ + observe: function AUS_observe(subject, topic, data) { + switch (topic) { + case "post-update-processing": + if (readStatusFile(getUpdatesDir()) == STATE_SUCCEEDED) { + // The active update needs to be copied to the first update in the + // updates.xml early during startup to support post update actions + // (bug 1301288). + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate.state = STATE_SUCCEEDED; + um.saveUpdates(); + Services.prefs.setBoolPref(PREF_APP_UPDATE_POSTUPDATE, true); + } + + if (Services.appinfo.ID in APPID_TO_TOPIC) { + // Delay post-update processing to ensure that possible update + // dialogs are shown in front of the app window, if possible. + // See bug 311614. + Services.obs.addObserver(this, APPID_TO_TOPIC[Services.appinfo.ID], + false); + break; + } + // intentional fallthrough + case "sessionstore-windows-restored": + case "mail-startup-done": + case "xul-window-visible": + if (Services.appinfo.ID in APPID_TO_TOPIC) { + Services.obs.removeObserver(this, + APPID_TO_TOPIC[Services.appinfo.ID]); + } + // intentional fallthrough + case "test-post-update-processing": + // Clean up any extant updates + this._postUpdateProcessing(); + break; + case "network:offline-status-changed": + this._offlineStatusChanged(data); + break; + case "nsPref:changed": + if (data == PREF_APP_UPDATE_LOG) { + gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); + } + break; + case "profile-change-net-teardown": // fall thru + case "xpcom-shutdown": + Services.obs.removeObserver(this, topic); + Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this); + + if (AppConstants.platform == "win" && gUpdateMutexHandle) { + // If we hold the update mutex, let it go! + // The OS would clean this up sometime after shutdown, + // but that would have no guarantee on timing. + closeHandle(gUpdateMutexHandle); + } + if (this._retryTimer) { + this._retryTimer.cancel(); + } + + this.pauseDownload(); + // Prevent leaking the downloader (bug 454964) + this._downloader = null; + break; + } + }, + + /** + * The following needs to happen during the post-update-processing + * notification from nsUpdateServiceStub.js: + * 1. post update processing + * 2. resume of a download that was in progress during a previous session + * 3. start of a complete update download after the failure to apply a partial + * update + */ + + /** + * Perform post-processing on updates lingering in the updates directory + * from a previous application session - either report install failures (and + * optionally attempt to fetch a different version if appropriate) or + * notify the user of install success. + */ + _postUpdateProcessing: function AUS__postUpdateProcessing() { + if (!this.canCheckForUpdates) { + LOG("UpdateService:_postUpdateProcessing - unable to check for " + + "updates... returning early"); + return; + } + + if (!this.canApplyUpdates) { + LOG("UpdateService:_postUpdateProcessing - unable to apply " + + "updates... returning early"); + // If the update is present in the update directly somehow, + // it would prevent us from notifying the user of futher updates. + cleanupActiveUpdate(); + return; + } + + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + var update = um.activeUpdate; + var status = readStatusFile(getUpdatesDir()); + pingStateAndStatusCodes(update, true, status); + // STATE_NONE status typically means that the update.status file is present + // but a background download error occurred. + if (status == STATE_NONE) { + LOG("UpdateService:_postUpdateProcessing - no status, no update"); + cleanupActiveUpdate(); + return; + } + + // Handle the case when the update is the same or older than the current + // version and nsUpdateDriver.cpp skipped updating due to the version being + // older than the current version. + if (update && update.appVersion && + (status == STATE_PENDING || status == STATE_PENDING_SERVICE || + status == STATE_APPLIED || status == STATE_APPLIED_SERVICE || + status == STATE_PENDING_ELEVATE)) { + if (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 || + Services.vc.compare(update.appVersion, Services.appinfo.version) == 0 && + update.buildID == Services.appinfo.appBuildID) { + LOG("UpdateService:_postUpdateProcessing - removing update for older " + + "or same application version"); + cleanupActiveUpdate(); + return; + } + } + + if (AppConstants.platform == "gonk") { + // This code is called very early in the boot process, before we've even + // had a chance to setup the UI so we can give feedback to the user. + // + // Since the download may be occuring over a link which has associated + // cost, we want to require user-consent before resuming the download. + // Also, applying an already downloaded update now is undesireable, + // since the phone will look dead while the update is being applied. + // Applying the update can take several minutes. Instead we wait until + // the UI is initialized so it is possible to give feedback to and get + // consent to update from the user. + if (isInterruptedUpdate(status)) { + LOG("UpdateService:_postUpdateProcessing - interrupted update detected - wait for user consent"); + return; + } + } + + if (status == STATE_DOWNLOADING) { + LOG("UpdateService:_postUpdateProcessing - patch found in downloading " + + "state"); + if (update && update.state != STATE_SUCCEEDED) { + // Resume download + status = this.downloadUpdate(update, true); + if (status == STATE_NONE) + cleanupActiveUpdate(); + } + return; + } + + if (status == STATE_APPLYING) { + // This indicates that the background updater service is in either of the + // following two states: + // 1. It is in the process of applying an update in the background, and + // we just happen to be racing against that. + // 2. It has failed to apply an update for some reason, and we hit this + // case because the updater process has set the update status to + // applying, but has never finished. + // In order to differentiate between these two states, we look at the + // state field of the update object. If it's "pending", then we know + // that this is the first time we're hitting this case, so we switch + // that state to "applying" and we just wait and hope for the best. + // If it's "applying", we know that we've already been here once, so + // we really want to start from a clean state. + if (update && + (update.state == STATE_PENDING || + update.state == STATE_PENDING_SERVICE)) { + LOG("UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the first time"); + update.state = STATE_APPLYING; + um.saveUpdates(); + } else { // We get here even if we don't have an update object + LOG("UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the second time"); + cleanupActiveUpdate(); + } + return; + } + + if (AppConstants.platform == "gonk") { + // The update is only applied but not selected to be installed + if (status == STATE_APPLIED && update && update.isOSUpdate) { + LOG("UpdateService:_postUpdateProcessing - update staged as applied found"); + return; + } + + if (status == STATE_APPLIED_OS && update && update.isOSUpdate) { + // In gonk, we need to check for OS update status after startup, since + // the recovery partition won't write to update.status for us + let recoveryService = Cc["@mozilla.org/recovery-service;1"]. + getService(Ci.nsIRecoveryService); + let fotaStatus = recoveryService.getFotaUpdateStatus(); + switch (fotaStatus) { + case Ci.nsIRecoveryService.FOTA_UPDATE_SUCCESS: + status = STATE_SUCCEEDED; + break; + case Ci.nsIRecoveryService.FOTA_UPDATE_FAIL: + status = STATE_FAILED + ": " + FOTA_GENERAL_ERROR; + break; + case Ci.nsIRecoveryService.FOTA_UPDATE_UNKNOWN: + default: + status = STATE_FAILED + ": " + FOTA_UNKNOWN_ERROR; + break; + } + } + } + + if (!update) { + if (status != STATE_SUCCEEDED) { + LOG("UpdateService:_postUpdateProcessing - previous patch failed " + + "and no patch available"); + cleanupActiveUpdate(); + return; + } + update = new Update(null); + } + + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_FAILED && parts[1]) { + update.errorCode = parseInt(parts[1]); + } + + + if (status != STATE_SUCCEEDED) { + // Since the update didn't succeed save a copy of the active update's + // current state to the updates.xml so it is possible to track failures. + um.saveUpdates(); + // Rotate the update logs so the update log isn't removed. By passing + // false the patch directory won't be removed. + cleanUpUpdatesDir(false); + } + + if (status == STATE_SUCCEEDED) { + update.statusText = gUpdateBundle.GetStringFromName("installSuccess"); + + // Update the patch's metadata. + um.activeUpdate = update; + + // Done with this update. Clean it up. + cleanupActiveUpdate(); + } else if (status == STATE_PENDING_ELEVATE) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateElevationRequired(); + return; + } else { + // If there was an I/O error it is assumed that the patch is not invalid + // and it is set to pending so an attempt to apply it again will happen + // when the application is restarted. + if (update.state == STATE_FAILED && update.errorCode) { + if (handleUpdateFailure(update, update.errorCode)) { + return; + } + } + + // Something went wrong with the patch application process. + handleFallbackToCompleteUpdate(update, false); + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + } + }, + + /** + * Register an observer when the network comes online, so we can short-circuit + * the app.update.interval when there isn't connectivity + */ + _registerOnlineObserver: function AUS__registerOnlineObserver() { + if (this._registeredOnlineObserver) { + LOG("UpdateService:_registerOnlineObserver - observer already registered"); + return; + } + + LOG("UpdateService:_registerOnlineObserver - waiting for the network to " + + "be online, then forcing another check"); + + Services.obs.addObserver(this, "network:offline-status-changed", false); + this._registeredOnlineObserver = true; + }, + + /** + * Called from the network:offline-status-changed observer. + */ + _offlineStatusChanged: function AUS__offlineStatusChanged(status) { + if (status !== "online") { + return; + } + + Services.obs.removeObserver(this, "network:offline-status-changed"); + this._registeredOnlineObserver = false; + + LOG("UpdateService:_offlineStatusChanged - network is online, forcing " + + "another background check"); + + // the background checker is contained in notify + this._attemptResume(); + }, + + onCheckComplete: function AUS_onCheckComplete(request, updates, updateCount) { + this._selectAndInstallUpdate(updates); + }, + + onError: function AUS_onError(request, update) { + LOG("UpdateService:onError - error during background update. error code: " + + update.errorCode + ", status text: " + update.statusText); + + if (update.errorCode == NETWORK_ERROR_OFFLINE) { + // Register an online observer to try again + this._registerOnlineObserver(); + if (this._pingSuffix) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE); + } + return; + } + + // Send the error code to telemetry + AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode); + update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES; + let errCount = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDERRORS, 0); + errCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount); + // Don't allow the preference to set a value greater than 20 for max errors. + let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20); + + if (errCount >= maxErrors) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT); + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT); + } + }, + + /** + * Called when a connection should be resumed + */ + _attemptResume: function AUS_attemptResume() { + LOG("UpdateService:_attemptResume"); + // If a download is in progress, then resume it. + if (this._downloader && this._downloader._patch && + this._downloader._patch.state == STATE_DOWNLOADING && + this._downloader._update) { + LOG("UpdateService:_attemptResume - _patch.state: " + + this._downloader._patch.state); + // Make sure downloading is the state for selectPatch to work correctly + writeStatusFile(getUpdatesDir(), STATE_DOWNLOADING); + var status = this.downloadUpdate(this._downloader._update, + this._downloader.background); + LOG("UpdateService:_attemptResume - downloadUpdate status: " + status); + if (status == STATE_NONE) { + cleanupActiveUpdate(); + } + return; + } + + this.backgroundChecker.checkForUpdates(this, false); + }, + + /** + * Notified when a timer fires + * @param timer + * The timer that fired + */ + notify: function AUS_notify(timer) { + this._checkForBackgroundUpdates(true); + }, + + /** + * See nsIUpdateService.idl + */ + checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() { + this._checkForBackgroundUpdates(false); + }, + + // The suffix used for background update check telemetry histogram ID's. + get _pingSuffix() { + return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL; + }, + + /** + * Checks for updates in the background. + * @param isNotify + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates(isNotify) { + this._isNotify = isNotify; + + // Histogram IDs: + // UPDATE_PING_COUNT_EXTERNAL + // UPDATE_PING_COUNT_NOTIFY + AUSTLMY.pingGeneric("UPDATE_PING_COUNT_" + this._pingSuffix, + true, false); + + // Histogram IDs: + // UPDATE_UNABLE_TO_APPLY_EXTERNAL + // UPDATE_UNABLE_TO_APPLY_NOTIFY + AUSTLMY.pingGeneric("UPDATE_UNABLE_TO_APPLY_" + this._pingSuffix, + getCanApplyUpdates(), true); + // Histogram IDs: + // UPDATE_CANNOT_STAGE_EXTERNAL + // UPDATE_CANNOT_STAGE_NOTIFY + AUSTLMY.pingGeneric("UPDATE_CANNOT_STAGE_" + this._pingSuffix, + getCanStageUpdates(), true); + // Histogram IDs: + // UPDATE_INVALID_LASTUPDATETIME_EXTERNAL + // UPDATE_INVALID_LASTUPDATETIME_NOTIFY + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY + AUSTLMY.pingLastUpdateTime(this._pingSuffix); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_ENABLED_" + this._pingSuffix, + PREF_APP_UPDATE_ENABLED, true, true); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_AUTO_" + this._pingSuffix, + PREF_APP_UPDATE_AUTO, true, true); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_" + + this._pingSuffix, + PREF_APP_UPDATE_STAGING_ENABLED, true, true); + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + // Histogram IDs: + // UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL + // UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_" + this._pingSuffix, + PREF_APP_UPDATE_CANCELATIONS, 0, 0); + } + if (AppConstants.platform == "macosx") { + // Histogram IDs: + // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_EXTERNAL + // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_OSX_" + + this._pingSuffix, + PREF_APP_UPDATE_CANCELATIONS_OSX, 0, 0); + } + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_" + + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ENABLED, true); + // Histogram IDs: + // UPDATE_PREF_SERVICE_ERRORS_EXTERNAL + // UPDATE_PREF_SERVICE_ERRORS_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_SERVICE_ERRORS_" + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ERRORS, 0, 0); + if (AppConstants.platform == "win") { + // Histogram IDs: + // UPDATE_SERVICE_INSTALLED_EXTERNAL + // UPDATE_SERVICE_INSTALLED_NOTIFY + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY + AUSTLMY.pingServiceInstallStatus(this._pingSuffix, isServiceInstalled()); + } + } + + // If a download is in progress or the patch has been staged do nothing. + if (this.isDownloading) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING); + return; + } + + if (this._downloader && this._downloader.patchIsStaged) { + let readState = readStatusFile(getUpdatesDir()); + if (readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + readState == STATE_PENDING_ELEVATE) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED); + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_STAGED); + } + return; + } + + let validUpdateURL = true; + try { + this.backgroundChecker.getUpdateURL(false); + } catch (e) { + validUpdateURL = false; + } + // The following checks are done here so they can be differentiated from + // foreground checks. + if (!UpdateUtils.OSVersion) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION); + } else if (!UpdateUtils.ABI) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI); + } else if (!validUpdateURL) { + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_INVALID_DEFAULT_URL); + } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED); + } else if (!hasUpdateMutex()) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX); + } else if (!gCanCheckForUpdates) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK); + } else if (!this.backgroundChecker._enabled) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_FOR_SESSION); + } + + this.backgroundChecker.checkForUpdates(this, false); + }, + + /** + * Determine the update from the specified updates that should be offered. + * If both valid major and minor updates are available the minor update will + * be offered. + * @param updates + * An array of available nsIUpdate items + * @return The nsIUpdate to offer. + */ + selectUpdate: function AUS_selectUpdate(updates) { + if (updates.length == 0) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND); + return null; + } + + // The ping for unsupported is sent after the call to showPrompt. + if (updates.length == 1 && updates[0].unsupported) { + return updates[0]; + } + + // Choose the newest of the available minor and major updates. + var majorUpdate = null; + var minorUpdate = null; + var vc = Services.vc; + let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND; + + updates.forEach(function(aUpdate) { + // Ignore updates for older versions of the application and updates for + // the same version of the application with the same build ID. + if (vc.compare(aUpdate.appVersion, Services.appinfo.version) < 0 || + vc.compare(aUpdate.appVersion, Services.appinfo.version) == 0 && + aUpdate.buildID == Services.appinfo.appBuildID) { + LOG("UpdateService:selectUpdate - skipping update because the " + + "update's application version is less than the current " + + "application version"); + lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION; + return; + } + + // Skip the update if the user responded with "never" to this update's + // application version and the update specifies showNeverForVersion + // (see bug 350636). + let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + aUpdate.appVersion; + if (aUpdate.showNeverForVersion && + getPref("getBoolPref", neverPrefName, false)) { + LOG("UpdateService:selectUpdate - skipping update because the " + + "preference " + neverPrefName + " is true"); + lastCheckCode = AUSTLMY.CHK_UPDATE_NEVER_PREF; + return; + } + + switch (aUpdate.type) { + case "major": + if (!majorUpdate) + majorUpdate = aUpdate; + else if (vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0) + majorUpdate = aUpdate; + break; + case "minor": + if (!minorUpdate) + minorUpdate = aUpdate; + else if (vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0) + minorUpdate = aUpdate; + break; + default: + LOG("UpdateService:selectUpdate - skipping unknown update type: " + + aUpdate.type); + lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE; + break; + } + }); + + let update = minorUpdate || majorUpdate; + if (AppConstants.platform == "macosx" && update) { + if (getElevationRequired()) { + let installAttemptVersion = getPref("getCharPref", + PREF_APP_UPDATE_ELEVATE_VERSION, + null); + if (vc.compare(installAttemptVersion, update.appVersion) != 0) { + Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_VERSION, + update.appVersion); + if (Services.prefs.prefHasUserValue( + PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } else { + let numCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX, 0); + let rejectedVersion = getPref("getCharPref", + PREF_APP_UPDATE_ELEVATE_NEVER, ""); + let maxCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX); + if (numCancels >= maxCancels) { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but the user has exceeded the max " + + "number of elevation attempts."); + update.elevationFailure = true; + AUSTLMY.pingCheckCode( + this._pingSuffix, + AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION); + } else if (vc.compare(rejectedVersion, update.appVersion) == 0) { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but elevation is disabled for this " + + "version."); + update.elevationFailure = true; + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION); + } else { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install the update."); + } + } + } else { + // Clear elevation-related prefs since they no longer apply (the user + // may have gained write access to the Firefox directory or an update + // was executed with a different profile). + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } + } else if (!update) { + AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode); + } + + return update; + }, + + /** + * Determine which of the specified updates should be installed and begin the + * download/installation process or notify the user about the update. + * @param updates + * An array of available updates + */ + _selectAndInstallUpdate: function AUS__selectAndInstallUpdate(updates) { + // Return early if there's an active update. The user is already aware and + // is downloading or performed some user action to prevent notification. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + if (um.activeUpdate) { + if (AppConstants.platform == "gonk") { + // For gonk, the user isn't necessarily aware of the update, so we need + // to show the prompt to make sure. + this._showPrompt(um.activeUpdate); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE); + return; + } + + var updateEnabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); + if (!updateEnabled) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED); + LOG("UpdateService:_selectAndInstallUpdate - not prompting because " + + "update is disabled"); + return; + } + + var update = this.selectUpdate(updates, updates.length); + if (!update || update.elevationFailure) { + return; + } + + if (update.unsupported) { + LOG("UpdateService:_selectAndInstallUpdate - update not supported for " + + "this system"); + if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) { + LOG("UpdateService:_selectAndInstallUpdate - notifying that the " + + "update is not supported for this system"); + this._showPrompt(update); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED); + return; + } + + if (!getCanApplyUpdates()) { + LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " + + "apply updates... prompting"); + this._showPrompt(update); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY); + return; + } + + /** + * From this point on there are two possible outcomes: + * 1. download and install the update automatically + * 2. notify the user about the availability of an update + * + * Notes: + * a) if the app.update.auto preference is false then automatic download and + * install is disabled and the user will be notified. + * b) if the update has a showPrompt attribute the user will be notified. + * + * If the update when it is first read does not have an appVersion attribute + * the following deprecated behavior will occur: + * Update Type Outcome + * Major Notify + * Minor Auto Install + */ + if (update.showPrompt) { + LOG("UpdateService:_selectAndInstallUpdate - prompting because the " + + "update snippet specified showPrompt"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET); + this._showPrompt(update); + return; + } + + if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) { + LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " + + "install is disabled"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF); + this._showPrompt(update); + return; + } + + LOG("UpdateService:_selectAndInstallUpdate - download the update"); + let status = this.downloadUpdate(update, true); + if (status == STATE_NONE) { + cleanupActiveUpdate(); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE); + }, + + _showPrompt: function AUS__showPrompt(update) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateAvailable(update); + }, + + /** + * The Checker used for background update checks. + */ + _backgroundChecker: null, + + /** + * See nsIUpdateService.idl + */ + get backgroundChecker() { + if (!this._backgroundChecker) + this._backgroundChecker = new Checker(); + return this._backgroundChecker; + }, + + /** + * See nsIUpdateService.idl + */ + get canCheckForUpdates() { + return gCanCheckForUpdates && hasUpdateMutex(); + }, + + /** + * See nsIUpdateService.idl + */ + get elevationRequired() { + return getElevationRequired(); + }, + + /** + * See nsIUpdateService.idl + */ + get canApplyUpdates() { + return getCanApplyUpdates() && hasUpdateMutex(); + }, + + /** + * See nsIUpdateService.idl + */ + get canStageUpdates() { + return getCanStageUpdates(); + }, + + /** + * See nsIUpdateService.idl + */ + get isOtherInstanceHandlingUpdates() { + return !hasUpdateMutex(); + }, + + + /** + * See nsIUpdateService.idl + */ + addDownloadListener: function AUS_addDownloadListener(listener) { + if (!this._downloader) { + LOG("UpdateService:addDownloadListener - no downloader!"); + return; + } + this._downloader.addDownloadListener(listener); + }, + + /** + * See nsIUpdateService.idl + */ + removeDownloadListener: function AUS_removeDownloadListener(listener) { + if (!this._downloader) { + LOG("UpdateService:removeDownloadListener - no downloader!"); + return; + } + this._downloader.removeDownloadListener(listener); + }, + + /** + * See nsIUpdateService.idl + */ + downloadUpdate: function AUS_downloadUpdate(update, background) { + if (!update) + throw Cr.NS_ERROR_NULL_POINTER; + + // Don't download the update if the update's version is less than the + // current application's version or the update's version is the same as the + // application's version and the build ID is the same as the application's + // build ID. + if (update.appVersion && + (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 || + update.buildID && update.buildID == Services.appinfo.appBuildID && + update.appVersion == Services.appinfo.version)) { + LOG("UpdateService:downloadUpdate - canceling download of update since " + + "it is for an earlier or same application version and build ID.\n" + + "current application version: " + Services.appinfo.version + "\n" + + "update application version : " + update.appVersion + "\n" + + "current build ID: " + Services.appinfo.appBuildID + "\n" + + "update build ID : " + update.buildID); + cleanupActiveUpdate(); + return STATE_NONE; + } + + // If a download request is in progress vs. a download ready to resume + if (this.isDownloading) { + if (update.isCompleteUpdate == this._downloader.isCompleteUpdate && + background == this._downloader.background) { + LOG("UpdateService:downloadUpdate - no support for downloading more " + + "than one update at a time"); + return readStatusFile(getUpdatesDir()); + } + this._downloader.cancel(); + } + if (AppConstants.platform == "gonk") { + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + let activeUpdate = um.activeUpdate; + if (activeUpdate && + (activeUpdate.appVersion != update.appVersion || + activeUpdate.buildID != update.buildID)) { + // We have an activeUpdate (which presumably was interrupted), and are + // about start downloading a new one. Make sure we remove all traces + // of the active one (otherwise we'll start appending the new update.mar + // the the one that's been partially downloaded). + LOG("UpdateService:downloadUpdate - removing stale active update."); + cleanupActiveUpdate(); + } + } + // Set the previous application version prior to downloading the update. + update.previousAppVersion = Services.appinfo.version; + this._downloader = new Downloader(background, this); + return this._downloader.downloadUpdate(update); + }, + + /** + * See nsIUpdateService.idl + */ + pauseDownload: function AUS_pauseDownload() { + if (this.isDownloading) { + this._downloader.cancel(); + } else if (this._retryTimer) { + // Download status is still consider as 'downloading' during retry. + // We need to cancel both retry and download at this stage. + this._retryTimer.cancel(); + this._retryTimer = null; + this._downloader.cancel(); + } + }, + + /** + * See nsIUpdateService.idl + */ + getUpdatesDirectory: getUpdatesDir, + + /** + * See nsIUpdateService.idl + */ + get isDownloading() { + return this._downloader && this._downloader.isBusy; + }, + + /** + * See nsIUpdateService.idl + */ + applyOsUpdate: function AUS_applyOsUpdate(aUpdate) { + if (!aUpdate.isOSUpdate || aUpdate.state != STATE_APPLIED) { + aUpdate.statusText = "fota-state-error"; + throw Cr.NS_ERROR_FAILURE; + } + + aUpdate.QueryInterface(Ci.nsIWritablePropertyBag); + let osApplyToDir = aUpdate.getProperty("osApplyToDir"); + + if (!osApplyToDir) { + LOG("UpdateService:applyOsUpdate - Error: osApplyToDir is not defined" + + "in the nsIUpdate!"); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR); + handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); + return; + } + + let updateFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + updateFile.initWithPath(osApplyToDir + "/update.zip"); + if (!updateFile.exists()) { + LOG("UpdateService:applyOsUpdate - Error: OS update is not found at " + + updateFile.path); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR); + handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); + return; + } + + writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED_OS); + LOG("UpdateService:applyOsUpdate - Rebooting into recovery to apply " + + "FOTA update: " + updateFile.path); + try { + let recoveryService = Cc["@mozilla.org/recovery-service;1"] + .getService(Ci.nsIRecoveryService); + recoveryService.installFotaUpdate(updateFile.path); + } catch (e) { + LOG("UpdateService:applyOsUpdate - Error: Couldn't reboot into recovery" + + " to apply FOTA update " + updateFile.path); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_RECOVERY_ERROR); + writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED); + handleUpdateFailure(aUpdate, FOTA_RECOVERY_ERROR); + } + }, + + classID: UPDATESERVICE_CID, + classInfo: XPCOMUtils.generateCI({classID: UPDATESERVICE_CID, + contractID: UPDATESERVICE_CONTRACTID, + interfaces: [Ci.nsIApplicationUpdateService, + Ci.nsITimerCallback, + Ci.nsIObserver], + flags: Ci.nsIClassInfo.SINGLETON}), + + _xpcom_factory: UpdateServiceFactory, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIApplicationUpdateService, + Ci.nsIUpdateCheckListener, + Ci.nsITimerCallback, + Ci.nsIObserver]) +}; + +/** + * A service to manage active and past updates. + * @constructor + */ +function UpdateManager() { + // Ensure the Active Update file is loaded + var updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (updates.length > 0) { + // Under some edgecases such as Windows system restore the active-update.xml + // will contain a pending update without the status file which will return + // STATE_NONE. To recover from this situation clean the updates dir and + // rewrite the active-update.xml file without the broken update. + if (readStatusFile(getUpdatesDir()) == STATE_NONE) { + cleanUpUpdatesDir(); + this._writeUpdatesToXMLFile([], getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + } + else + this._activeUpdate = updates[0]; + } +} +UpdateManager.prototype = { + /** + * All previously downloaded and installed updates, as an array of nsIUpdate + * objects. + */ + _updates: null, + + /** + * The current actively downloading/installing update, as a nsIUpdate object. + */ + _activeUpdate: null, + + /** + * Handle Observer Service notifications + * @param subject + * The subject of the notification + * @param topic + * The notification name + * @param data + * Additional data + */ + observe: function UM_observe(subject, topic, data) { + // Hack to be able to run and cleanup tests by reloading the update data. + if (topic == "um-reload-update-data") { + this._updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_UPDATES_XML])); + this._activeUpdate = null; + var updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (updates.length > 0) + this._activeUpdate = updates[0]; + } + }, + + /** + * Loads an updates.xml formatted file into an array of nsIUpdate items. + * @param file + * A nsIFile for the updates.xml file + * @return The array of nsIUpdate items held in the file. + */ + _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(file) { + if (!file.exists()) { + LOG("UpdateManager:_loadXMLFileIntoArray: XML file does not exist"); + return []; + } + + var result = []; + var fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + try { + var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + var doc = parser.parseFromStream(fileStream, "UTF-8", + fileStream.available(), "text/xml"); + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + var updateCount = doc.documentElement.childNodes.length; + for (var i = 0; i < updateCount; ++i) { + var updateElement = doc.documentElement.childNodes.item(i); + if (updateElement.nodeType != ELEMENT_NODE || + updateElement.localName != "update") + continue; + + updateElement.QueryInterface(Ci.nsIDOMElement); + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("UpdateManager:_loadXMLFileIntoArray - invalid update"); + continue; + } + result.push(update); + } + } + catch (e) { + LOG("UpdateManager:_loadXMLFileIntoArray - error constructing update " + + "list. Exception: " + e); + } + fileStream.close(); + return result; + }, + + /** + * Load the update manager, initializing state from state files. + */ + _ensureUpdates: function UM__ensureUpdates() { + if (!this._updates) { + this._updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_UPDATES_XML])); + var activeUpdates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (activeUpdates.length > 0) + this._activeUpdate = activeUpdates[0]; + } + }, + + /** + * See nsIUpdateService.idl + */ + getUpdateAt: function UM_getUpdateAt(index) { + this._ensureUpdates(); + return this._updates[index]; + }, + + /** + * See nsIUpdateService.idl + */ + get updateCount() { + this._ensureUpdates(); + return this._updates.length; + }, + + /** + * See nsIUpdateService.idl + */ + get activeUpdate() { + if (this._activeUpdate && + this._activeUpdate.channel != UpdateUtils.UpdateChannel) { + LOG("UpdateManager:get activeUpdate - channel has changed, " + + "reloading default preferences to workaround bug 802022"); + // Workaround to get distribution preferences loaded (Bug 774618). This + // can be removed after bug 802022 is fixed. + let prefSvc = Services.prefs.QueryInterface(Ci.nsIObserver); + prefSvc.observe(null, "reload-default-prefs", null); + if (this._activeUpdate.channel != UpdateUtils.UpdateChannel) { + // User switched channels, clear out any old active updates and remove + // partial downloads + this._activeUpdate = null; + this.saveUpdates(); + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + } + } + return this._activeUpdate; + }, + set activeUpdate(activeUpdate) { + this._addUpdate(activeUpdate); + this._activeUpdate = activeUpdate; + if (!activeUpdate) { + // If |activeUpdate| is null, we have updated both lists - the active list + // and the history list, so we want to write both files. + this.saveUpdates(); + } + else + this._writeUpdatesToXMLFile([this._activeUpdate], + getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + return activeUpdate; + }, + + /** + * Add an update to the Updates list. If the item already exists in the list, + * replace the existing value with the new value. + * @param update + * The nsIUpdate object to add. + */ + _addUpdate: function UM__addUpdate(update) { + if (!update) + return; + this._ensureUpdates(); + if (this._updates) { + for (var i = 0; i < this._updates.length; ++i) { + // Keep all update entries with a state of STATE_FAILED and replace the + // first update entry that has the same application version and build ID + // if it exists. This allows the update history to only have one success + // entry for an update and entries for all failed updates. + if (update.state != STATE_FAILED && + this._updates[i] && + this._updates[i].state != STATE_FAILED && + this._updates[i].appVersion == update.appVersion && + this._updates[i].buildID == update.buildID) { + // Replace the existing entry with the new value, updating + // all metadata. + this._updates[i] = update; + return; + } + } + } + // Otherwise add it to the front of the list. + this._updates.unshift(update); + }, + + /** + * Serializes an array of updates to an XML file + * @param updates + * An array of nsIUpdate objects + * @param file + * The nsIFile object to serialize to + */ + _writeUpdatesToXMLFile: function UM__writeUpdatesToXMLFile(updates, file) { + var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + var modeFlags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | + FileUtils.MODE_TRUNCATE; + if (!file.exists()) { + file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + } + fos.init(file, modeFlags, FileUtils.PERMS_FILE, 0); + + try { + var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + const EMPTY_UPDATES_DOCUMENT = "<?xml version=\"1.0\"?><updates xmlns=\"http://www.mozilla.org/2005/app-update\"></updates>"; + var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT, "text/xml"); + + for (var i = 0; i < updates.length; ++i) { + // If appVersion isn't defined don't add the update. This happens when + // cleaning up invalid updates (e.g. incorrect channel). + if (updates[i] && updates[i].appVersion) { + doc.documentElement.appendChild(updates[i].serialize(doc)); + } + } + + var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]. + createInstance(Ci.nsIDOMSerializer); + serializer.serializeToStream(doc.documentElement, fos, null); + } catch (e) { + } + + FileUtils.closeSafeFileOutputStream(fos); + }, + + /** + * See nsIUpdateService.idl + */ + saveUpdates: function UM_saveUpdates() { + this._writeUpdatesToXMLFile([this._activeUpdate], + getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + if (this._activeUpdate) + this._addUpdate(this._activeUpdate); + + this._ensureUpdates(); + // Don't write updates that have a temporary state to the updates.xml file. + if (this._updates) { + let updates = this._updates.slice(); + for (let i = updates.length - 1; i >= 0; --i) { + let state = updates[i].state; + if (state == STATE_NONE || state == STATE_DOWNLOADING || + state == STATE_APPLIED || state == STATE_APPLIED_SERVICE || + state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + updates.splice(i, 1); + } + } + + this._writeUpdatesToXMLFile(updates.slice(0, 20), + getUpdateFile([FILE_UPDATES_XML])); + } + }, + + /** + * See nsIUpdateService.idl + */ + refreshUpdateStatus: function UM_refreshUpdateStatus() { + var update = this._activeUpdate; + if (!update) { + return; + } + var status = readStatusFile(getUpdatesDir()); + pingStateAndStatusCodes(update, false, status); + var parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_FAILED && parts[1]) { + update.errorCode = parseInt(parts[1]); + } + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + // Save a copy of the active update's current state to the updates.xml so + // it is possible to track failures. + um.saveUpdates(); + + // Rotate the update logs so the update log isn't removed if a complete + // update is downloaded. By passing false the patch directory won't be + // removed. + cleanUpUpdatesDir(false); + + if (update.state == STATE_FAILED && parts[1]) { + if (!handleUpdateFailure(update, parts[1])) { + handleFallbackToCompleteUpdate(update, true); + } + + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("stagingFailed", "true"); + } + if (update.state == STATE_APPLIED && shouldUseService()) { + writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE); + } + + // Send an observer notification which the update wizard uses in + // order to update its UI. + LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " + + "the update was staged. state: " + update.state + ", status: " + status); + Services.obs.notifyObservers(null, "update-staged", update.state); + + if (AppConstants.platform == "gonk") { + // Do this after everything else, since it will likely cause the app to + // shut down. + if (update.state == STATE_APPLIED) { + // Notify the user that an update has been staged and is ready for + // installation (i.e. that they should restart the application). We do + // not notify on failed update attempts. + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(update, true); + } else { + releaseSDCardMountLock(); + } + return; + } + // Only prompt when the UI isn't already open. + let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); + if (Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME) || + windowType && Services.wm.getMostRecentWindow(windowType)) { + return; + } + + if (update.state == STATE_APPLIED || + update.state == STATE_APPLIED_SERVICE || + update.state == STATE_PENDING || + update.state == STATE_PENDING_SERVICE || + update.state == STATE_PENDING_ELEVATE) { + // Notify the user that an update has been staged and is ready for + // installation (i.e. that they should restart the application). + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(update, true); + } + }, + + /** + * See nsIUpdateService.idl + */ + elevationOptedIn: function UM_elevationOptedIn() { + // The user has been been made aware that the update requires elevation. + let update = this._activeUpdate; + if (!update) { + return; + } + let status = readStatusFile(getUpdatesDir()); + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_PENDING_ELEVATE) { + // Proceed with the pending update. + // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to + // proceed with an elevated update". As long as we see this state, we will + // notify the user of the availability of an update that requires + // elevation. |elevationOptedIn| (this function) is called when the user + // gives us approval to proceed, so we want to switch to STATE_PENDING. + // The updater then detects whether or not elevation is required and + // displays the elevation prompt if necessary. This last step does not + // depend on the state in the status file. + writeStatusFile(getUpdatesDir(), STATE_PENDING); + } + }, + + /** + * See nsIUpdateService.idl + */ + cleanupActiveUpdate: function UM_cleanupActiveUpdate() { + cleanupActiveUpdate(); + }, + + classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager, Ci.nsIObserver]) +}; + +/** + * Checker + * Checks for new Updates + * @constructor + */ +function Checker() { +} +Checker.prototype = { + /** + * The XMLHttpRequest object that performs the connection. + */ + _request: null, + + /** + * The nsIUpdateCheckListener callback + */ + _callback: null, + + /** + * The URL of the update service XML file to connect to that contains details + * about available updates. + */ + getUpdateURL: function UC_getUpdateURL(force) { + this._forced = force; + + let url; + try { + url = Services.prefs.getDefaultBranch(null). + getCharPref(PREF_APP_UPDATE_URL); + } catch (e) { + } + + if (!url || url == "") { + LOG("Checker:getUpdateURL - update URL not defined"); + return null; + } + + url = UpdateUtils.formatUpdateURL(url); + + if (force) { + url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1"; + } + + LOG("Checker:getUpdateURL - update URL: " + url); + return url; + }, + + /** + * See nsIUpdateService.idl + */ + checkForUpdates: function UC_checkForUpdates(listener, force) { + LOG("Checker: checkForUpdates, force: " + force); + if (!listener) + throw Cr.NS_ERROR_NULL_POINTER; + + Services.obs.notifyObservers(null, "update-check-start", null); + + var url = this.getUpdateURL(force); + if (!url || (!this.enabled && !force)) + return; + + this._request = new XMLHttpRequest(); + this._request.open("GET", url, true); + this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false); + // Prevent the request from reading from the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + // Prevent the request from writing to the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + // Disable cutting edge features, like TLS 1.3, where middleboxes might brick us + this._request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true; + + this._request.overrideMimeType("text/xml"); + // The Cache-Control header is only interpreted by proxies and the + // final destination. It does not help if a resource is already + // cached locally. + this._request.setRequestHeader("Cache-Control", "no-cache"); + // HTTP/1.0 servers might not implement Cache-Control and + // might only implement Pragma: no-cache + this._request.setRequestHeader("Pragma", "no-cache"); + + var self = this; + this._request.addEventListener("error", function(event) { self.onError(event); }, false); + this._request.addEventListener("load", function(event) { self.onLoad(event); }, false); + + LOG("Checker:checkForUpdates - sending request to: " + url); + this._request.send(null); + + this._callback = listener; + }, + + /** + * Returns an array of nsIUpdate objects discovered by the update check. + * @throws if the XML document element node name is not updates. + */ + get _updates() { + var updatesElement = this._request.responseXML.documentElement; + if (!updatesElement) { + LOG("Checker:_updates get - empty updates document?!"); + return []; + } + + if (updatesElement.nodeName != "updates") { + LOG("Checker:_updates get - unexpected node name!"); + throw new Error("Unexpected node name, expected: updates, got: " + + updatesElement.nodeName); + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + var updates = []; + for (var i = 0; i < updatesElement.childNodes.length; ++i) { + var updateElement = updatesElement.childNodes.item(i); + if (updateElement.nodeType != ELEMENT_NODE || + updateElement.localName != "update") + continue; + + updateElement.QueryInterface(Ci.nsIDOMElement); + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("Checker:_updates get - invalid <update/>, ignoring..."); + continue; + } + update.serviceURL = this.getUpdateURL(this._forced); + update.channel = UpdateUtils.UpdateChannel; + updates.push(update); + } + + return updates; + }, + + /** + * Returns the status code for the XMLHttpRequest + */ + _getChannelStatus: function UC__getChannelStatus(request) { + var status = 0; + try { + status = request.status; + } + catch (e) { + } + + if (status == 0) + status = request.channel.QueryInterface(Ci.nsIRequest).status; + return status; + }, + + _isHttpStatusCode: function UC__isHttpStatusCode(status) { + return status >= 100 && status <= 599; + }, + + /** + * The XMLHttpRequest succeeded and the document was loaded. + * @param event + * The nsIDOMEvent for the load + */ + onLoad: function UC_onLoad(event) { + LOG("Checker:onLoad - request completed downloading document"); + + try { + // Analyze the resulting DOM and determine the set of updates. + var updates = this._updates; + LOG("Checker:onLoad - number of updates available: " + updates.length); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + // Tell the callback about the updates + this._callback.onCheckComplete(event.target, updates, updates.length); + } catch (e) { + LOG("Checker:onLoad - there was a problem checking for updates. " + + "Exception: " + e); + var request = event.target; + var status = this._getChannelStatus(request); + LOG("Checker:onLoad - request.status: " + status); + var update = new Update(null); + update.errorCode = status; + update.statusText = getStatusTextFromCode(status, 404); + + if (this._isHttpStatusCode(status)) { + update.errorCode = HTTP_ERROR_OFFSET + status; + } + + this._callback.onError(request, update); + } + + this._callback = null; + this._request = null; + }, + + /** + * There was an error of some kind during the XMLHttpRequest + * @param event + * The nsIDOMEvent for the error + */ + onError: function UC_onError(event) { + var request = event.target; + var status = this._getChannelStatus(request); + LOG("Checker:onError - request.status: " + status); + + // If we can't find an error string specific to this status code, + // just use the 200 message from above, which means everything + // "looks" fine but there was probably an XML error or a bogus file. + var update = new Update(null); + update.errorCode = status; + update.statusText = getStatusTextFromCode(status, 200); + + if (status == Cr.NS_ERROR_OFFLINE) { + // We use a separate constant here because nsIUpdate.errorCode is signed + update.errorCode = NETWORK_ERROR_OFFLINE; + } else if (this._isHttpStatusCode(status)) { + update.errorCode = HTTP_ERROR_OFFSET + status; + } + + this._callback.onError(request, update); + this._request = null; + }, + + /** + * Whether or not we are allowed to do update checking. + */ + _enabled: true, + get enabled() { + return getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true) && + gCanCheckForUpdates && hasUpdateMutex() && this._enabled; + }, + + /** + * See nsIUpdateService.idl + */ + stopChecking: function UC_stopChecking(duration) { + // Always stop the current check + if (this._request) + this._request.abort(); + + switch (duration) { + case Ci.nsIUpdateChecker.CURRENT_SESSION: + this._enabled = false; + break; + case Ci.nsIUpdateChecker.ANY_CHECKS: + this._enabled = false; + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, this._enabled); + break; + } + + this._callback = null; + }, + + classID: Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateChecker]) +}; + +/** + * Manages the download of updates + * @param background + * Whether or not this downloader is operating in background + * update mode. + * @param updateService + * The update service that created this downloader. + * @constructor + */ +function Downloader(background, updateService) { + LOG("Creating Downloader"); + this.background = background; + this.updateService = updateService; +} +Downloader.prototype = { + /** + * The nsIUpdatePatch that we are downloading + */ + _patch: null, + + /** + * The nsIUpdate that we are downloading + */ + _update: null, + + /** + * The nsIIncrementalDownload object handling the download + */ + _request: null, + + /** + * Whether or not the update being downloaded is a complete replacement of + * the user's existing installation or a patch representing the difference + * between the new version and the previous version. + */ + isCompleteUpdate: null, + + /** + * Cancels the active download. + */ + cancel: function Downloader_cancel(cancelError) { + LOG("Downloader: cancel"); + if (cancelError === undefined) { + cancelError = Cr.NS_BINDING_ABORTED; + } + if (this._request && this._request instanceof Ci.nsIRequest) { + this._request.cancel(cancelError); + } + if (AppConstants.platform == "gonk") { + releaseSDCardMountLock(); + } + }, + + /** + * Whether or not a patch has been downloaded and staged for installation. + */ + get patchIsStaged() { + var readState = readStatusFile(getUpdatesDir()); + // Note that if we decide to download and apply new updates after another + // update has been successfully applied in the background, we need to stop + // checking for the APPLIED state here. + return readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + readState == STATE_PENDING_ELEVATE || + readState == STATE_APPLIED || readState == STATE_APPLIED_SERVICE; + }, + + /** + * Verify the downloaded file. We assume that the download is complete at + * this point. + */ + _verifyDownload: function Downloader__verifyDownload() { + LOG("Downloader:_verifyDownload called"); + if (!this._request) { + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST); + return false; + } + + let destination = this._request.destination; + + // Ensure that the file size matches the expected file size. + if (destination.fileSize != this._patch.size) { + LOG("Downloader:_verifyDownload downloaded size != expected size."); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL); + return false; + } + + LOG("Downloader:_verifyDownload downloaded size == expected size."); + + // The hash check is not necessary when mar signatures are used to verify + // the downloaded mar file. + if (AppConstants.MOZ_VERIFY_MAR_SIGNATURE) { + return true; + } + + let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fileStream.init(destination, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + + let digest; + try { + let hash = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + var hashFunction = Ci.nsICryptoHash[this._patch.hashFunction.toUpperCase()]; + if (hashFunction == undefined) { + throw Cr.NS_ERROR_UNEXPECTED; + } + hash.init(hashFunction); + hash.updateFromStream(fileStream, -1); + // NOTE: For now, we assume that the format of _patch.hashValue is hex + // encoded binary (such as what is typically output by programs like + // sha1sum). In the future, this may change to base64 depending on how + // we choose to compute these hashes. + digest = binaryToHex(hash.finish(false)); + } catch (e) { + LOG("Downloader:_verifyDownload - failed to compute hash of the " + + "downloaded update archive"); + digest = ""; + } + + fileStream.close(); + + if (digest == this._patch.hashValue.toLowerCase()) { + LOG("Downloader:_verifyDownload hashes match."); + return true; + } + + LOG("Downloader:_verifyDownload hashes do not match. "); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_NO_HASH_MATCH); + return false; + }, + + /** + * Select the patch to use given the current state of updateDir and the given + * set of update patches. + * @param update + * A nsIUpdate object to select a patch from + * @param updateDir + * A nsIFile representing the update directory + * @return A nsIUpdatePatch object to download + */ + _selectPatch: function Downloader__selectPatch(update, updateDir) { + // Given an update to download, we will always try to download the patch + // for a partial update over the patch for a full update. + + /** + * Return the first UpdatePatch with the given type. + * @param type + * The type of the patch ("complete" or "partial") + * @return A nsIUpdatePatch object matching the type specified + */ + function getPatchOfType(type) { + for (var i = 0; i < update.patchCount; ++i) { + var patch = update.getPatchAt(i); + if (patch && patch.type == type) + return patch; + } + return null; + } + + // Look to see if any of the patches in the Update object has been + // pre-selected for download, otherwise we must figure out which one + // to select ourselves. + var selectedPatch = update.selectedPatch; + + var state = readStatusFile(updateDir); + + // If this is a patch that we know about, then select it. If it is a patch + // that we do not know about, then remove it and use our default logic. + var useComplete = false; + if (selectedPatch) { + LOG("Downloader:_selectPatch - found existing patch with state: " + + state); + if (state == STATE_DOWNLOADING) { + LOG("Downloader:_selectPatch - resuming download"); + return selectedPatch; + } + + if (AppConstants.platform == "gonk") { + if (state == STATE_PENDING || state == STATE_APPLYING) { + LOG("Downloader:_selectPatch - resuming interrupted apply"); + return selectedPatch; + } + if (state == STATE_APPLIED) { + LOG("Downloader:_selectPatch - already downloaded and staged"); + return null; + } + } else if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + LOG("Downloader:_selectPatch - already downloaded and staged"); + return null; + } + + if (update && selectedPatch.type == "complete") { + // This is a pretty fatal error. Just bail. + LOG("Downloader:_selectPatch - failed to apply complete patch!"); + writeStatusFile(updateDir, STATE_NONE); + writeVersionFile(getUpdatesDir(), null); + return null; + } + + // Something went wrong when we tried to apply the previous patch. + // Try the complete patch next time. + useComplete = true; + selectedPatch = null; + } + + // If we were not able to discover an update from a previous download, we + // select the best patch from the given set. + var partialPatch = getPatchOfType("partial"); + if (!useComplete) + selectedPatch = partialPatch; + if (!selectedPatch) { + if (partialPatch) + partialPatch.selected = false; + selectedPatch = getPatchOfType("complete"); + } + + // Restore the updateDir since we may have deleted it. + updateDir = getUpdatesDir(); + + // if update only contains a partial patch, selectedPatch == null here if + // the partial patch has been attempted and fails and we're trying to get a + // complete patch + if (selectedPatch) + selectedPatch.selected = true; + + update.isCompleteUpdate = useComplete; + + // Reset the Active Update object on the Update Manager and flush the + // Active Update DB. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate = update; + + return selectedPatch; + }, + + /** + * Whether or not we are currently downloading something. + */ + get isBusy() { + return this._request != null; + }, + + /** + * Get the nsIFile to use for downloading the active update's selected patch + */ + _getUpdateArchiveFile: function Downloader__getUpdateArchiveFile() { + var updateArchive; + if (AppConstants.platform == "gonk") { + try { + updateArchive = FileUtils.getDir(KEY_UPDATE_ARCHIVE_DIR, [], true); + } catch (e) { + return null; + } + } else { + updateArchive = getUpdatesDir().clone(); + } + + updateArchive.append(FILE_UPDATE_MAR); + return updateArchive; + }, + + /** + * Download and stage the given update. + * @param update + * A nsIUpdate object to download a patch for. Cannot be null. + */ + downloadUpdate: function Downloader_downloadUpdate(update) { + LOG("UpdateService:_downloadUpdate"); + if (!update) { + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE); + throw Cr.NS_ERROR_NULL_POINTER; + } + + var updateDir = getUpdatesDir(); + + this._update = update; + + // This function may return null, which indicates that there are no patches + // to download. + this._patch = this._selectPatch(update, updateDir); + if (!this._patch) { + LOG("Downloader:downloadUpdate - no patch to download"); + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH); + return readStatusFile(updateDir); + } + this.isCompleteUpdate = this._patch.type == "complete"; + + let patchFile = null; + + // Only used by gonk + let status = STATE_NONE; + if (AppConstants.platform == "gonk") { + status = readStatusFile(updateDir); + if (isInterruptedUpdate(status)) { + LOG("Downloader:downloadUpdate - interruptted update"); + // The update was interrupted. Try to locate the existing patch file. + // For an interrupted download, this allows a resume rather than a + // re-download. + patchFile = getFileFromUpdateLink(updateDir); + if (!patchFile) { + // No link file. We'll just assume that the update.mar is in the + // update directory. + patchFile = updateDir.clone(); + patchFile.append(FILE_UPDATE_MAR); + } + if (patchFile.exists()) { + LOG("Downloader:downloadUpdate - resuming with patchFile " + patchFile.path); + if (patchFile.fileSize == this._patch.size) { + LOG("Downloader:downloadUpdate - patchFile appears to be fully downloaded"); + // Bump the status along so that we don't try to redownload again. + if (getElevationRequired()) { + status = STATE_PENDING_ELEVATE; + } else { + status = STATE_PENDING; + } + } + } else { + LOG("Downloader:downloadUpdate - patchFile " + patchFile.path + + " doesn't exist - performing full download"); + // The patchfile doesn't exist, we might as well treat this like + // a new download. + patchFile = null; + } + if (patchFile && status != STATE_DOWNLOADING) { + // It looks like the patch was downloaded, but got interrupted while it + // was being verified or applied. So we'll fake the downloading portion. + + if (getElevationRequired()) { + writeStatusFile(updateDir, STATE_PENDING_ELEVATE); + } else { + writeStatusFile(updateDir, STATE_PENDING); + } + + // Since the code expects the onStopRequest callback to happen + // asynchronously (And you have to call AUS_addDownloadListener + // after calling AUS_downloadUpdate) we need to defer this. + + this._downloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._downloadTimer.initWithCallback(function() { + this._downloadTimer = null; + // Send a fake onStopRequest. Filling in the destination allows + // _verifyDownload to work, and then the update will be applied. + this._request = {destination: patchFile}; + this.onStopRequest(this._request, null, Cr.NS_OK); + }.bind(this), 0, Ci.nsITimer.TYPE_ONE_SHOT); + + // Returning STATE_DOWNLOADING makes UpdatePrompt think we're + // downloading. The onStopRequest that we spoofed above will make it + // look like the download finished. + return STATE_DOWNLOADING; + } + } + } + + if (!patchFile) { + // Find a place to put the patchfile that we're going to download. + patchFile = this._getUpdateArchiveFile(); + } + if (!patchFile) { + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_NO_PATCH_FILE); + return STATE_NONE; + } + + if (AppConstants.platform == "gonk") { + if (patchFile.path.indexOf(updateDir.path) != 0) { + // The patchFile is in a directory which is different from the + // updateDir, create a link file. + writeLinkFile(updateDir, patchFile); + + if (!isInterruptedUpdate(status) && patchFile.exists()) { + // Remove stale patchFile + patchFile.remove(false); + } + } + } + + update.QueryInterface(Ci.nsIPropertyBag); + let interval = this.background ? update.getProperty("backgroundInterval") + : DOWNLOAD_FOREGROUND_INTERVAL; + + var uri = Services.io.newURI(this._patch.URL, null, null); + LOG("Downloader:downloadUpdate - url: " + uri.spec + ", path: " + + patchFile.path + ", interval: " + interval); + + this._request = Cc["@mozilla.org/network/incremental-download;1"]. + createInstance(Ci.nsIIncrementalDownload); + this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval); + this._request.start(this, null); + + writeStatusFile(updateDir, STATE_DOWNLOADING); + this._patch.QueryInterface(Ci.nsIWritablePropertyBag); + this._patch.state = STATE_DOWNLOADING; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.saveUpdates(); + return STATE_DOWNLOADING; + }, + + /** + * An array of download listeners to notify when we receive + * nsIRequestObserver or nsIProgressEventSink method calls. + */ + _listeners: [], + + /** + * Adds a listener to the download process + * @param listener + * A download listener, implementing nsIRequestObserver and + * nsIProgressEventSink + */ + addDownloadListener: function Downloader_addDownloadListener(listener) { + for (var i = 0; i < this._listeners.length; ++i) { + if (this._listeners[i] == listener) + return; + } + this._listeners.push(listener); + }, + + /** + * Removes a download listener + * @param listener + * The listener to remove. + */ + removeDownloadListener: function Downloader_removeDownloadListener(listener) { + for (var i = 0; i < this._listeners.length; ++i) { + if (this._listeners[i] == listener) { + this._listeners.splice(i, 1); + return; + } + } + }, + + /** + * When the async request begins + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + */ + onStartRequest: function Downloader_onStartRequest(request, context) { + if (request instanceof Ci.nsIIncrementalDownload) + LOG("Downloader:onStartRequest - original URI spec: " + request.URI.spec + + ", final URI spec: " + request.finalURI.spec); + // Always set finalURL in onStartRequest since it can change. + this._patch.finalURL = request.finalURI.spec; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.saveUpdates(); + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) + listeners[i].onStartRequest(request, context); + }, + + /** + * When new data has been downloaded + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param progress + * The current number of bytes transferred + * @param maxProgress + * The total number of bytes that must be transferred + */ + onProgress: function Downloader_onProgress(request, context, progress, + maxProgress) { + LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress); + + if (progress > this._patch.size) { + LOG("Downloader:onProgress - progress: " + progress + + " is higher than patch size: " + this._patch.size); + // It's important that we use a different code than + // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference + // between a hash error and a wrong download error. + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + if (maxProgress != this._patch.size) { + LOG("Downloader:onProgress - maxProgress: " + maxProgress + + " is not equal to expected patch size: " + this._patch.size); + // It's important that we use a different code than + // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference + // between a hash error and a wrong download error. + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + var listener = listeners[i]; + if (listener instanceof Ci.nsIProgressEventSink) + listener.onProgress(request, context, progress, maxProgress); + } + this.updateService._consecutiveSocketErrors = 0; + }, + + /** + * When we have new status text + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * A status code + * @param statusText + * Human readable version of |status| + */ + onStatus: function Downloader_onStatus(request, context, status, statusText) { + LOG("Downloader:onStatus - status: " + status + ", statusText: " + + statusText); + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + var listener = listeners[i]; + if (listener instanceof Ci.nsIProgressEventSink) + listener.onStatus(request, context, status, statusText); + } + }, + + /** + * When data transfer ceases + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * Status code containing the reason for the cessation. + */ + onStopRequest: function Downloader_onStopRequest(request, context, status) { + if (request instanceof Ci.nsIIncrementalDownload) + LOG("Downloader:onStopRequest - original URI spec: " + request.URI.spec + + ", final URI spec: " + request.finalURI.spec + ", status: " + status); + + // XXX ehsan shouldShowPrompt should always be false here. + // But what happens when there is already a UI showing? + var state = this._patch.state; + var shouldShowPrompt = false; + var shouldRegisterOnlineObserver = false; + var shouldRetrySoon = false; + var deleteActiveUpdate = false; + var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT, + DEFAULT_SOCKET_RETRYTIMEOUT); + // Prevent the preference from setting a value greater than 10000. + retryTimeout = Math.min(retryTimeout, 10000); + var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_MAXERRORS, + DEFAULT_SOCKET_MAX_ERRORS); + // Prevent the preference from setting a value greater than 20. + maxFail = Math.min(maxFail, 20); + LOG("Downloader:onStopRequest - status: " + status + ", " + + "current fail: " + this.updateService._consecutiveSocketErrors + ", " + + "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout); + if (Components.isSuccessCode(status)) { + if (this._verifyDownload()) { + if (shouldUseService()) { + state = STATE_PENDING_SERVICE; + } else if (getElevationRequired()) { + state = STATE_PENDING_ELEVATE; + } else { + state = STATE_PENDING; + } + if (this.background) { + shouldShowPrompt = !getCanStageUpdates(); + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS); + + // Tell the updater.exe we're ready to apply. + writeStatusFile(getUpdatesDir(), state); + writeVersionFile(getUpdatesDir(), this._update.appVersion); + this._update.installDate = (new Date()).getTime(); + this._update.statusText = gUpdateBundle.GetStringFromName("installPending"); + } + else { + LOG("Downloader:onStopRequest - download verification failed"); + state = STATE_DOWNLOAD_FAILED; + status = Cr.NS_ERROR_CORRUPTED_CONTENT; + + // Yes, this code is a string. + const vfCode = "verification_failed"; + var message = getStatusTextFromCode(vfCode, vfCode); + this._update.statusText = message; + + if (this._update.isCompleteUpdate || this._update.patchCount != 2) + deleteActiveUpdate = true; + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + } + } else if (status == Cr.NS_ERROR_OFFLINE) { + // Register an online observer to try again. + // The online observer will continue the incremental download by + // calling downloadUpdate on the active update which continues + // downloading the file from where it was. + LOG("Downloader:onStopRequest - offline, register online observer: true"); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_RETRY_OFFLINE); + shouldRegisterOnlineObserver = true; + deleteActiveUpdate = false; + // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, + // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned + // when disconnecting the internet while a download of a MAR is in + // progress. There may be others but I have not encountered them during + // testing. + } else if ((status == Cr.NS_ERROR_NET_TIMEOUT || + status == Cr.NS_ERROR_CONNECTION_REFUSED || + status == Cr.NS_ERROR_NET_RESET || + status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) && + this.updateService._consecutiveSocketErrors < maxFail) { + LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true"); + let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED; + if (status == Cr.NS_ERROR_NET_TIMEOUT) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT; + } else if (status == Cr.NS_ERROR_NET_RESET) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET; + } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) { + dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + shouldRetrySoon = true; + deleteActiveUpdate = false; + } else if (status != Cr.NS_BINDING_ABORTED && + status != Cr.NS_ERROR_ABORT) { + LOG("Downloader:onStopRequest - non-verification failure"); + let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED; + if (status == Cr.NS_ERROR_ABORT) { + dwnldCode = AUSTLMY.DWNLD_ERR_ABORT; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + + // Some sort of other failure, log this in the |statusText| property + state = STATE_DOWNLOAD_FAILED; + + // XXXben - if |request| (The Incremental Download) provided a means + // for accessing the http channel we could do more here. + + this._update.statusText = getStatusTextFromCode(status, + Cr.NS_BINDING_FAILED); + + if (AppConstants.platform == "gonk") { + // bug891009: On FirefoxOS, manaully retry OTA download will reuse + // the Update object. We need to remove selected patch so that download + // can be triggered again successfully. + this._update.selectedPatch.selected = false; + } + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + + deleteActiveUpdate = true; + } + LOG("Downloader:onStopRequest - setting state to: " + state); + this._patch.state = state; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + if (deleteActiveUpdate) { + this._update.installDate = (new Date()).getTime(); + um.activeUpdate = null; + } + else if (um.activeUpdate) { + um.activeUpdate.state = state; + } + um.saveUpdates(); + + // Only notify listeners about the stopped state if we + // aren't handling an internal retry. + if (!shouldRetrySoon && !shouldRegisterOnlineObserver) { + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + listeners[i].onStopRequest(request, context, status); + } + } + + this._request = null; + + if (state == STATE_DOWNLOAD_FAILED) { + var allFailed = true; + // Check if there is a complete update patch that can be downloaded. + if (!this._update.isCompleteUpdate && this._update.patchCount == 2) { + LOG("Downloader:onStopRequest - verification of patch failed, " + + "downloading complete update patch"); + this._update.isCompleteUpdate = true; + let updateStatus = this.downloadUpdate(this._update); + + if (updateStatus == STATE_NONE) { + cleanupActiveUpdate(); + } else { + allFailed = false; + } + } + + if (allFailed) { + LOG("Downloader:onStopRequest - all update patch downloads failed"); + // If the update UI is not open (e.g. the user closed the window while + // downloading) and if at any point this was a foreground download + // notify the user about the error. If the update was a background + // update there is no notification since the user won't be expecting it. + if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) { + this._update.QueryInterface(Ci.nsIWritablePropertyBag); + if (this._update.getProperty("foregroundDownload") == "true") { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(this._update); + } + } + + if (AppConstants.platform == "gonk") { + // We always forward errors in B2G, since Gaia controls the update UI + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(this._update); + } + + // Prevent leaking the update object (bug 454964). + this._update = null; + } + // A complete download has been initiated or the failure was handled. + return; + } + + if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + if (getCanStageUpdates()) { + LOG("Downloader:onStopRequest - attempting to stage update: " + + this._update.name); + + // Initiate the update in the background + try { + Cc["@mozilla.org/updates/update-processor;1"]. + createInstance(Ci.nsIUpdateProcessor). + processUpdate(this._update); + } catch (e) { + // Fail gracefully in case the application does not support the update + // processor service. + LOG("Downloader:onStopRequest - failed to stage update. Exception: " + + e); + if (this.background) { + shouldShowPrompt = true; + } + } + } + } + + // Do this after *everything* else, since it will likely cause the app + // to shut down. + if (shouldShowPrompt) { + // Notify the user that an update has been downloaded and is ready for + // installation (i.e. that they should restart the application). We do + // not notify on failed update attempts. + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(this._update, true); + } + + if (shouldRegisterOnlineObserver) { + LOG("Downloader:onStopRequest - Registering online observer"); + this.updateService._registerOnlineObserver(); + } else if (shouldRetrySoon) { + LOG("Downloader:onStopRequest - Retrying soon"); + this.updateService._consecutiveSocketErrors++; + if (this.updateService._retryTimer) { + this.updateService._retryTimer.cancel(); + } + this.updateService._retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.updateService._retryTimer.initWithCallback(function() { + this._attemptResume(); + }.bind(this.updateService), retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + // Prevent leaking the update object (bug 454964) + this._update = null; + } + }, + + /** + * See nsIInterfaceRequestor.idl + */ + getInterface: function Downloader_getInterface(iid) { + // The network request may require proxy authentication, so provide the + // default nsIAuthPrompt if requested. + if (iid.equals(Ci.nsIAuthPrompt)) { + var prompt = Cc["@mozilla.org/network/default-auth-prompt;1"]. + createInstance(); + return prompt.QueryInterface(iid); + } + throw Cr.NS_NOINTERFACE; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor]) +}; + +/** + * UpdatePrompt + * An object which can prompt the user with information about updates, request + * action, etc. Embedding clients can override this component with one that + * invokes a native front end. + * @constructor + */ +function UpdatePrompt() { +} +UpdatePrompt.prototype = { + /** + * See nsIUpdateService.idl + */ + checkForUpdates: function UP_checkForUpdates() { + if (this._getAltUpdateWindow()) + return; + + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, + null, null); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateAvailable: function UP_showUpdateAvailable(update) { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getUpdateWindow() || this._getAltUpdateWindow()) { + return; + } + + this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "updatesavailable", update); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) { + if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) { + return; + } + // Trigger the display of the hamburger menu badge. + Services.obs.notifyObservers(null, "update-downloaded", update.state); + + if (this._getAltUpdateWindow()) + return; + + if (background) { + this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", update); + } else { + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", update); + } + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateError: function UP_showUpdateError(update) { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getAltUpdateWindow()) + return; + + // In some cases, we want to just show a simple alert dialog. + // Replace with Array.prototype.includes when it has stabilized. + if (update.state == STATE_FAILED && + (WRITE_ERRORS.indexOf(update.errorCode) != -1 || + update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR || + update.errorCode == FOTA_GENERAL_ERROR || + update.errorCode == FOTA_FILE_OPERATION_ERROR || + update.errorCode == FOTA_RECOVERY_ERROR || + update.errorCode == FOTA_UNKNOWN_ERROR)) { + var title = gUpdateBundle.GetStringFromName("updaterIOErrorTitle"); + var text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg", + [Services.appinfo.name, + Services.appinfo.name], 2); + Services.ww.getNewPrompter(null).alert(title, text); + return; + } + + if (update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) { + this._showUIWhenIdle(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, null, update); + return; + } + + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, + "errors", update); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateHistory: function UP_showUpdateHistory(parent) { + this._showUI(parent, URI_UPDATE_HISTORY_DIALOG, "modal,dialog=yes", + "Update:History", null, null); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateElevationRequired: function UP_showUpdateElevationRequired() { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getAltUpdateWindow()) { + return; + } + + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", um.activeUpdate); + }, + + /** + * Returns the update window if present. + */ + _getUpdateWindow: function UP__getUpdateWindow() { + return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME); + }, + + /** + * Returns an alternative update window if present. When a window with this + * windowtype is open the application update service won't open the normal + * application update user interface window. + */ + _getAltUpdateWindow: function UP__getAltUpdateWindow() { + let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); + if (!windowType) + return null; + return Services.wm.getMostRecentWindow(windowType); + }, + + /** + * Display the update UI after the prompt wait time has elapsed. + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUnobtrusiveUI: function UP__showUnobUI(parent, uri, features, name, page, + update) { + var observer = { + updatePrompt: this, + service: null, + timer: null, + notify: function () { + // the user hasn't restarted yet => prompt when idle + this.service.removeObserver(this, "quit-application"); + // If the update window is already open skip showing the UI + if (this.updatePrompt._getUpdateWindow()) + return; + this.updatePrompt._showUIWhenIdle(parent, uri, features, name, page, update); + }, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "quit-application": + if (this.timer) + this.timer.cancel(); + this.service.removeObserver(this, "quit-application"); + break; + } + } + }; + + // bug 534090 - show the UI for update available notifications when the + // the system has been idle for at least IDLE_TIME. + if (page == "updatesavailable") { + var idleService = Cc["@mozilla.org/widget/idleservice;1"]. + getService(Ci.nsIIdleService); + // Don't allow the preference to set a value greater than 600 seconds for the idle time. + const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600); + if (idleService.idleTime / 1000 >= IDLE_TIME) { + this._showUI(parent, uri, features, name, page, update); + return; + } + } + + observer.service = Services.obs; + observer.service.addObserver(observer, "quit-application", false); + + // bug 534090 - show the UI when idle for update available notifications. + if (page == "updatesavailable") { + this._showUIWhenIdle(parent, uri, features, name, page, update); + return; + } + + // Give the user x seconds to react before prompting as defined by + // promptWaitTime + observer.timer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + observer.timer.initWithCallback(observer, update.promptWaitTime * 1000, + observer.timer.TYPE_ONE_SHOT); + }, + + /** + * Show the UI when the user was idle + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUIWhenIdle: function UP__showUIWhenIdle(parent, uri, features, name, + page, update) { + var idleService = Cc["@mozilla.org/widget/idleservice;1"]. + getService(Ci.nsIIdleService); + + // Don't allow the preference to set a value greater than 600 seconds for the idle time. + const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600); + if (idleService.idleTime / 1000 >= IDLE_TIME) { + this._showUI(parent, uri, features, name, page, update); + } else { + var observer = { + updatePrompt: this, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "idle": + // If the update window is already open skip showing the UI + if (!this.updatePrompt._getUpdateWindow()) + this.updatePrompt._showUI(parent, uri, features, name, page, update); + // fall thru + case "quit-application": + idleService.removeIdleObserver(this, IDLE_TIME); + Services.obs.removeObserver(this, "quit-application"); + break; + } + } + }; + idleService.addIdleObserver(observer, IDLE_TIME); + Services.obs.addObserver(observer, "quit-application", false); + } + }, + + /** + * Show the Update Checking UI + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUI: function UP__showUI(parent, uri, features, name, page, update) { + var ary = null; + if (update) { + ary = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + ary.appendElement(update, /* weak =*/ false); + } + + var win = this._getUpdateWindow(); + if (win) { + if (page && "setCurrentPage" in win) + win.setCurrentPage(page); + win.focus(); + } + else { + var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no"; + if (features) + openFeatures += "," + features; + Services.ww.openWindow(parent, uri, "", openFeatures, ary); + } + }, + + classDescription: "Update Prompt", + contractID: "@mozilla.org/updates/update-prompt;1", + classID: Components.ID("{27ABA825-35B5-4018-9FDD-F99250A0E722}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt]) +}; + +var components = [UpdateService, Checker, UpdatePrompt, UpdateManager]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); |