/* -*- 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.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_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 PREF_APP_UPDATE_URL_OVERRIDE = "app.update.url.override"; 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"; 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_ELEVATE = "pending-elevate"; const STATE_APPLYING = "applying"; const STATE_APPLIED = "applied"; const STATE_APPLIED_OS = "applied-os"; 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 WRITE_ERROR_ACCESS_DENIED = 35; const WRITE_ERROR_CALLBACK_APP = 37; const FILESYSTEM_MOUNT_READWRITE_ERROR = 43; 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]; // 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 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; XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() { return Services.prefs.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) { #ifdef XP_WIN 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; #else throw Cr.NS_ERROR_NOT_IMPLEMENTED; #endif } /** * 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) { #ifdef XP_WIN 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); #else throw Cr.NS_ERROR_NOT_IMPLEMENTED; #endif } /** * 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() { #ifdef XP_WIN if (!gUpdateMutexHandle) { gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false); } return !!gUpdateMutexHandle; #else return true; #endif } /** * 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() { #ifdef XP_MACOSX 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"); #endif 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() { #ifndef XP_MACOSX try { let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); LOG("getCanApplyUpdates - testing write access " + updateTestFile.path); testWriteAccess(updateTestFile, false); #ifdef XP_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); } #endif // XP_WIN } catch (e) { LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e); // No write privileges to install directory return false; } #endif // !XP_MACOSX 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; #ifdef XP_MACOSX updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); #else updateTestFile = getInstallDirRoot(); updateTestFile.append(FILE_UPDATE_TEST); #endif LOG("gCanStageUpdatesSession - testing write access " + updateTestFile.path); testWriteAccess(updateTestFile, true); #ifndef XP_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); #endif // !XP_MACOSX } 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 (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) { LOG("getCanStageUpdates - staging updates is disabled by preference " + PREF_APP_UPDATE_STAGING_ENABLED); return false; } 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 = Services.prefs.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); } } /** * 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(); #ifdef XP_MACOSX // On Mac, we store the Updated.app directory inside the bundle directory. dir = dir.parent.parent; #endif 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); } /** * 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); // 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); } } } } /** * 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 = Services.prefs.getIntPref(PREF_APP_UPDATE_CANCELATIONS, 0); cancelations++; Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations); #ifdef XP_MACOSX let osxCancelations = Services.prefs.getIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX, 0); osxCancelations++; Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX, osxCancelations); let maxCancels = Services.prefs.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); #endif 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); } 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); } /** * 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 = Services.prefs.getIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 43200); this.backgroundInterval = Services.prefs.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); } 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 = Services.prefs.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); #ifdef XP_WIN if (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); } #endif 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()); // 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_APPLIED || 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 (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)) { 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 (!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(); return; } update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES; let errCount = Services.prefs.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(Services.prefs.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); } }, /** * 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); }, /** * 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; if (this._downloader && this._downloader.patchIsStaged) { return; } 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) { 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; updates.forEach(function(aUpdate) { // Ignore updates for older versions of the applications and updates for // the same version or build id of the application if (vc.compare(aUpdate.appVersion, Services.appinfo.version) < 0 || (vc.compare(aUpdate.appVersion, Services.appinfo.version) == 0 && aUpdate.buildID <= Services.appinfo.appBuildID) || (vc.compare(aUpdate.appVersion, Services.appinfo.version) == 0 && aUpdate.buildID == undefined)) { LOG("UpdateService:selectUpdate - skipping update because the " + "update's application version is less than or equal to " + "the current application 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 && Services.prefs.getBoolPref(neverPrefName, false)) { LOG("UpdateService:selectUpdate - skipping update because the " + "preference " + neverPrefName + " is true"); 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); break; } }); let update = minorUpdate || majorUpdate; #ifdef XP_MACOSX if (update) { if (getElevationRequired()) { let installAttemptVersion = Services.prefs.getCharPref( PREF_APP_UPDATE_ELEVATE_VERSION, ""); 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 = Services.prefs.getIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX, 0); let rejectedVersion = Services.prefs.getCharPref(PREF_APP_UPDATE_ELEVATE_NEVER, ""); let maxCancels = Services.prefs.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; } 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; } 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); } } } #endif 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) { return; } var updateEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED, true); if (!updateEnabled) { 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 (!Services.prefs.getBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) { LOG("UpdateService:_selectAndInstallUpdate - notifying that the " + "update is not supported for this system"); this._showPrompt(update); } return; } if (!getCanApplyUpdates()) { LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " + "apply updates... prompting"); this._showPrompt(update); 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"); this._showPrompt(update); return; } if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO, true)) { LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " + "install is disabled"); this._showPrompt(update); return; } LOG("UpdateService:_selectAndInstallUpdate - download the update"); let status = this.downloadUpdate(update, true); if (status == STATE_NONE) { cleanupActiveUpdate(); } }, _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(); } // 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!"); 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); 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); 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_PENDING || 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()); 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"); } // 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); // Only prompt when the UI isn't already open. let windowType = Services.prefs.getCharPref(PREF_APP_UPDATE_ALTWINDOWTYPE, ""); if (Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME) || windowType && Services.wm.getMostRecentWindow(windowType)) { return; } if (update.state == STATE_APPLIED || update.state == STATE_PENDING || 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; // Use the override URL if specified. let url = Services.prefs.getCharPref(PREF_APP_UPDATE_URL_OVERRIDE, ""); // Otherwise, construct the update URL from component parts. if (!url) { url = Services.prefs.getDefaultBranch(null). getCharPref(PREF_APP_UPDATE_URL, ""); } 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 Services.prefs.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); } }, /** * 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_ELEVATE || readState == STATE_APPLIED; }, /** * 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) { 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."); return false; } LOG("Downloader:_verifyDownload downloaded size == expected size."); #ifdef MOZ_VERIFY_MAR_SIGNATURE // The hash check is not necessary when mar signatures are used to verify // the downloaded mar file. return true; #endif 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. "); 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 (state == STATE_PENDING || 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; 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) { 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"); return readStatusFile(updateDir); } this.isCompleteUpdate = this._patch.type == "complete"; let patchFile = null; if (!patchFile) { // Find a place to put the patchfile that we're going to download. patchFile = this._getUpdateArchiveFile(); } if (!patchFile) { return STATE_NONE; } 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. 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. 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 = Services.prefs.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 = Services.prefs.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 (getElevationRequired()) { state = STATE_PENDING_ELEVATE; } else { state = STATE_PENDING; } if (this.background) { shouldShowPrompt = !getCanStageUpdates(); } // 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"); 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"); shouldRetrySoon = true; deleteActiveUpdate = false; } else if (status != Cr.NS_BINDING_ABORTED && status != Cr.NS_ERROR_ABORT) { LOG("Downloader:onStopRequest - non-verification failure"); // 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); // 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); } } // 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_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 (Services.prefs.getBoolPref(PREF_APP_UPDATE_SILENT, false) || this._getUpdateWindow() || this._getAltUpdateWindow()) { return; } this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, "updatesavailable", update); }, /** * See nsIUpdateService.idl */ showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) { if (background && Services.prefs.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; this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, "finishedBackground", update); }, /** * See nsIUpdateService.idl */ showUpdateError: function UP_showUpdateError(update) { if (Services.prefs.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 (Services.prefs.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 = Services.prefs.getCharPref(PREF_APP_UPDATE_ALTWINDOWTYPE, ""); 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(Services.prefs.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(Services.prefs.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);