/* 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/. */
/**
* This file provides logic to upgrade an old Firefox client to a modern
* Firefox.
*
* Please note this code is expected to work in Firefox 10 through 30. If you
* see usage of legacy patterns, that is why.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"log",
"manager",
];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
"resource://gre/modules/ctypes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
let Logging;
// The logging module has been moved around over the years.
try {
// It is currently at Log.jsm.
let ns = {};
Cu.import("resource://gre/modules/Log.jsm", ns);
Logging = ns.Log;
} catch (ex) {
try {
// It previously existed in services-common.
let ns = {};
Cu.import("resource://services-common/log4moz.js", ns);
Logging = ns.Log4Moz;
} catch (ex) {
// Before services-common, there was services-sync.
let ns = {};
Cu.import("resource://services-sync/log4moz.js", ns);
Logging = ns.Log4Moz;
}
}
/**
* A logging appender that buffers messages in memory.
*
* We use this so messages aren't lost before the hotfix is started and
* the log file is configured.
*/
function InMemoryAppender(formatter) {
Logging.Appender.call(this, formatter);
this.messages = [];
}
InMemoryAppender.prototype = {
__proto__: Logging.Appender.prototype,
append: function (message) {
if (message) {
this.messages.push(message);
}
}
};
/**
* Old file appender from legacy log4moz.js.
*
* The new one doesn't suit our needs because it doesn't do appends
* properly. And, the mismatch between synchronous and asynchronous behavior
* results in lots of extra complexity.
*
* This appender, while doing main thread I/O, makes our life easier.
* Most clients shouldn't be dropping lots of log events, so the main
* thread I/O should be in check.
*/
function MainThreadFileAppender(file, formatter) {
Logging.Appender.call(this, formatter);
this._file = file;
this._stream = null;
this._populateStream();
}
MainThreadFileAppender.prototype = {
__proto__: Logging.Appender.prototype,
_populateStream: function () {
// First create a raw stream. We can bail out early if that fails.
let os;
try {
os = FileUtils.openFileOutputStream(this._file,
FileUtils.MODE_WRONLY |
FileUtils.MODE_CREATE |
FileUtils.MODE_APPEND);
} catch (e) {
return null;
}
// Wrap the raw stream in an nsIConverterOutputStream. We can reuse
// the instance if we already have one.
this._stream = Cc["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Ci.nsIConverterOutputStream);
this._stream.init(os, "UTF-8", 4096,
this._stream.DEFAULT_REPLACEMENT_CHARACTER);
},
doAppend: function doAppend(message) {
if (!message || !this._stream) {
return;
}
// Different versions of Log.jsm insert the "\n" at different places.
if (message.charAt(message.length - 1) != "\n") {
message += "\n";
}
// If this fails, there's nothing much we can do. Sadness.
try {
this._stream.writeString(message);
} catch (e) { }
},
close: function () {
if (this._stream) {
this._stream.close();
this._stream = null;
}
},
flush: function () {
if (this._stream) {
this._stream.flush();
}
},
};
/**
* Log formatter that writes newline-delimited JSON arrays.
*/
function JSONFormatter() {
Logging.Formatter.call(this);
}
JSONFormatter.prototype = {
format: function (message) {
return JSON.stringify([
message.time,
message.level,
message.message,
]);
},
};
this.log = Logging.repository.getLogger("hotfix.autoupdate");
// Enable to make local testing easier.
//this.log.addAppender(new Logging.ConsoleAppender());
let memoryAppender = new InMemoryAppender();
this.log.addAppender(memoryAppender);
let gFileAppender;
/**
* Coordinates the background updating of old Firefox clients.
*
* From a high level, this type is responsible for upgrading old Firefox
* clients to a new, modern version. It does this by testing for upgrade
* applicability, downloading an installer, and executing that installer.
* There are UI elements that may be displayed to remind users to upgrade.
* There is also built-in reporting of results to Mozilla (i.e. a hotfix
* health report).
*
* This type has the following responsibilities:
*
* 1) Global and add-on state management. This includes configuration of
* the instance, filesystem and state management, and logging management.
*
* 2) Event coordination and orchestration.
*
* 3) Downloading an installer to local disk. This includes support for
* resuming interrupted downloads and verifying the downloaded file
* matches expectations.
*
* 4) Launching an installer.
*
* 5) Notifying/reminding the user about a pending upgrade.
*
* 6) Uploading analytics to Mozilla for diagnosis.
*
* 7) Generic utilities and support code.
*
* The code layout has attempted to isolate each responsibility so that
* code is grouped together.
*
* Workflow
* ========
*
* Upon installation, start() is called and we check to see if this add-on
* is applicable. If we are not applicable, the add-on is uninstalled.
* Not applicable cases include where automatic updates are disabled.
*
* If the add-on is applicable (meaning we are running on an old Firefox
* version and we qualify for an upgrade), we gather state and start the
* work to run the installer. See tryToDownloadAndUpdate().
*
* If the configured installer is not downloaded, we download it in the
* background. See _ensureInstallerDownloaded(). We support resuming
* partial downloads.
*
* When the installer is fully downloaded and ready to execute, we call
* _onInstallerReady(). On the first call, we attempt to execute the
* installer. If that goes well, the add-on is uninstalled and our work
* is done.
*
* We differentiate failed installations by their likelihood of recurring.
* We assume some failures such as permission declined or permission not
* allowed are transient (the next time the user tries they may click a
* different button or may have an administrator available to type in a
* password, etc). Non-transient errors result in immediate add-on uninstall.
*
* If transient install errors are detected, the add-on will display a
* pop-up notification of a pending Firefox upgrade on Firefox start-up.
* The pop-up will always be displayed on about:home (opening it if
* necessary). The notification will be displayed at most once per calendar
* day. These pop-ups will appear indefinitely until Firefox is upgraded.
* Note that these pop-ups will not be displayed if app updates are disabled
* (due to failing the applicability check).
*/
function UpgradeManager() {
this._s = null;
this._log = log;
this._prefs = Services.prefs.getBranch(this.PREFS_BRANCH);
this._locale = null;
this._stateDir = null;
this._logFile = null;
// nsIFile of JSON file holding our persisted state.
this._stateFile = null;
// URL of installer to download.
this._installerURL = null;
// Byte size of installer.
this._installerExpectedSize = null;
// Hex SHA-512 of installer.
this._installerExpectedHash = null;
// nsIFile of installer to execute.
this._installerFile = null;
// nsIFile of temporary file to download installer to.
this._installerTempFile = null;
// nsIFile of where to install Firefox.
this._targetDir = null;
// nsIFile of the .exe launcher that launches the installer.
this._launcherFile = null;
// nsIFile of the log file created by the launcher.
this._launcherLogFile = null;
// nsIFile of the log file created by the installer.
this._installerLogFile = null;
// nsIFile of the .ini file used by the installer.
this._installerIniFile = null;
// Whether this instance has been started.
this._started = false;
// Whether this instance has been destoryed.
this._destroyed = false;
// nsITimer managing retry attempts.
this._retryTimer = null;
// Callbacks for in-progress state save requests.
this._pendingStateQueue = [];
}
UpgradeManager.prototype = {
// Where to store our preferences.
PREFS_BRANCH: "hotfix.v20140527.01.",
// URL of tab that we should open notification on.
NEW_TAB_URL: "about:home",
// Where to find JSON describing installers.
INSTALLERS_URI: "chrome://firefox-hotfix/content/installers.json",
// Where to find the installer launcher executable.
LAUNCHER_URI: "chrome://firefox-hotfix/content/InstallerLauncher.exe",
// Upgrade versions of Firefox older than this. Versions equal to this
// will not be upgraded.
MAX_UPGRADE_VERSION: "29",
// Version to mark as upgraded.
UPGRADED_VERSION: "30",
// How often to retry failed downloads.
// Value was chosen arbitrarily.
DOWNLOAD_RETRY_MILLISECONDS: 60 * 60 * 1000, // 1 hour.
// How often to re-attempt failed installation.
// The value specified may not result in a notification: it's just how
// often to run our main routine that *may* take action.
INSTALL_RETRY_MILLISECONDS: 24 * 60 * 60 * 1000, // 1 day.
// Maximum number of times a reported successful download should fail
// before we give up and never try again.
MAX_DOWNLOAD_FAILURES: 10,
// Where to upload anonymous execution results to.
UPLOAD_URL: "https://hotfix.telemetry.mozilla.org/submit/hotfix/",
// Value of startup.homepage_override_url to set on successful upgrade.
UPGRADE_HOMEPAGE_OVERRIDE: "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/whatsnew/?oldversion=%OLD_VERSION%",
// URL to open upon a failed install.
FAILED_INSTALL_URL: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/failed-update",
/**
* Start execution.
*
* This performs start-up tasks for the extension.
*
* @param isInstall
* (bool) Whether the add-on is being installed as opposed to
* merely starting up like normal.
*/
start: function (isInstall) {
if (this._destroyed) {
this._log.warn("Cannot start an instance after it's been destroyed.");
return;
}
// We should always have a profile if this is called since bootstrap()
// is called after profile init.
this._stateDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
this._stateDir.append("hotfix-update");
try {
this._stateDir.create(Ci.nsIFile.DIRECTORY_TYPE, 493 /* 0755 */);
this._log.warn("Created state directory.");
} catch (ex) {
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
this._log.error("Error creating download dir: " + ex);
this._selfUninstall("PROFILE_PERMISSIONS_ERROR");
return;
}
}
if (!gFileAppender) {
this._logFile = this._stateDir.clone();
this._logFile.append("update.log");
let formatter = new JSONFormatter();
let appender = new MainThreadFileAppender(this._logFile, formatter);
log.addAppender(appender);
gFileAppender = appender;
// Now append buffered messages produced before we loaded into the
// file appender.
for each (let message in memoryAppender.messages) {
appender.append(message);
}
// And remove the memory appender since it no longer has a purpose.
log.removeAppender(memoryAppender);
memoryAppender = null;
}
// Wait for browser to load before continuing.
if (!isInstall) {
this._log.warn("Waiting for session restore to perform startup.");
let us = this;
Services.obs.addObserver(function obs() {
Services.obs.removeObserver(obs, "sessionstore-windows-restored");
us._startup(isInstall);
}, "sessionstore-windows-restored", false);
} else {
this._startup(isInstall);
}
},
/**
* Perform the low-level work of initializing.
*
* The instance is not usable until this is called. This should only
* be called once the browser has been fully initialized.
*/
_startup: function (isInstall) {
this._log.warn("Performing start-up tasks.");
this._stateFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
this._stateFile.append("hotfix.v20140527.01.json");
this._loadState(isInstall, function onStateLoaded(state) {
this._s = state;
this._onStateLoaded();
}.bind(this));
},
_onStateLoaded: function () {
// If Firefox closes while an install is running, our state is undefined.
// Try to ascertain what happened.
if (this._s._installInProgress) {
this._log.warn("State says install in progress. Reconciling.");
if (this._isUpgraded()) {
this._log.warn("Upgrade appears to have completed successfully.");
this._s._installInProgress = false;
this._s.installSuccesses++;
this._s.uninstallReason = "SUCCESSFUL_UPGRADE";
} else {
this._log.warn("Upgrade appears to not have worked. Will attempt again.");
this._s._installInProgress = false;
}
this._saveState();
}
// We need to initialize these early for forensic upload to work right.
// XCurProcD isn't reliable. See bug 1014194 comments 50 and on.
this._targetDir = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent;
this._installerLogFile = this._targetDir.clone();
this._installerLogFile.append("install.log");
this._installerIniFile = this._stateDir.clone();
this._installerIniFile.append("install.ini");
this._ensureLocaleLoaded(this._onLocaleLoaded.bind(this));
},
_onLocaleLoaded: function () {
// We have a few major branches we could take here. In chronological order:
//
// 1) The hotfix was just installed. We have no state. We need to determine
// whether the hotfix is applicable and continue running or uninstall
// as appropriate.
//
// 2) The hotfix is starting after it was installed on a prior browser
// session. We should resume where we left off.
//
// 3) The hotfix is starting after a completed upgrade. In this state, we
// want to report on the results of the upgrade using FHR.
if (!this._isHotfixApplicable()) {
// It looks like this is a first-time use on an incompatible client.
// Uninstall immediately.
if (!this._s._everCompatible) {
this._selfUninstall("NOT_APPLICABLE");
return;
}
// If we get here, the hotfix was marked as applicable before but is no
// longer so. The hotfix could have upgraded us to a modern client. Or,
// the client changed a setting or installed a different version
// manually that makes it no longer compatible.
if (!this._s.installSuccesses) {
this._selfUninstall("NO_LONGER_APPLICABLE");
return;
}
this._log.warn("Starting after successful upgrade.");
this._onPostUpgrade();
return;
}
// This gets set once and is never reset.
this._s._everCompatible = true;
// Date preferences can suffer from clock skew. Look for times in the
// future and reset.
if (this._s.firstNotifyDay && this._s.firstNotifyDay > this._daysSinceEpoch()) {
this._log.warn("Resetting firstNotifyDay because of apparent clock skew.");
this._s.firstNotifyDay = 0;
}
if (this._s.lastNotifyDay && this._s.lastNotifyDay > this._daysSinceEpoch()) {
this._log.warn("Resetting lastNotifyDay because of apparent clock skew.");
this._s.lastNotifyDay = 0;
}
// Back up the upgrade URL in case it is set.
if (Services.prefs.prefHasUserValue("startup.homepage_override_url")) {
// But only if we haven't backed it up yet (so we don't overrite the backup.)
if (!this._prefs.prefHasUserValue("override_url_backup_performed")) {
this._prefs.setCharPref("override_url_backup",
Services.prefs.getCharPref("startup.homepage_override_url"));
this._prefs.setBoolPref("override_url_backup_performed", true);
}
}
// If we can't obtain a locale, give up immediately.
// This should never happen. But you never know.
if (!this._locale) {
this._log.error("Could not obtain locale!");
this._selfUninstall("NO_LOCALE");
return;
}
let installerInfo = this._getInstallersJSON();
let builds = installerInfo.win32;
if (!(this._locale in builds)) {
this._log.error("No installer for locale: " + this._locale);
this._selfUninstall("UNSUPPORTED_LOCALE");
return;
}
this._installerURL = installerInfo.WIN32_INSTALLER_URL_PATTERN;
this._installerURL = this._installerURL.replace("%LOCALE%", this._locale);
this._installerExpectedSize = builds[this._locale][0];
this._installerExpectedHash = builds[this._locale][1];
this._installerFile = this._stateDir.clone();
this._installerFile.append(installerInfo.WIN32_INSTALLER_FILENAME);
this._installerTempFile = this._stateDir.clone();
this._installerTempFile.append(installerInfo.WIN32_INSTALLER_FILENAME + ".part");
this._log.warn("Installer URL: " + this._installerURL);
this._log.warn("Installer size (expected): " + this._installerExpectedSize);
this._log.warn("Installer hash (expected): " + this._installerExpectedHash);
this._launcherLogFile = this._stateDir.clone();
this._launcherLogFile.append("installer.log");
this._launcherFile = this._stateDir.clone();
// Give a friendly name so people don't get scared.
this._launcherFile.append("FirefoxInstallLauncher.exe");
this._saveState(function () {
this._log.warn("Finished start-up.");
if (this._started) {
return;
}
// One-time tasks go here.
browserWindowCall(injectCSS);
Services.wm.addListener(windowListener);
this._started = true;
this.tryToDownloadAndUpdate();
}.bind(this));
},
/**
* Try to ascertain the installer locale to use.
*
* When the callback is called, this._locale should have a value. It may
* be null if a locale could not be determined.
*/
_ensureLocaleLoaded: function (cb) {
if (this._locale) {
cb();
return;
}
// Modern versions of Firefox have the locale in omni.jar.
// We don't care about main thread I/O because omni.jar reads are
// negligible since omni.jar is loaded into memory.
for each (let res in ["app", "gre"]) {
try {
let url = "resource://" + res + "/update.locale";
let channel = Services.io.newChannel(url, null, null);
let is = channel.open();
this._locale = this._readInputStream(is);
if (this._locale) {
cb();
return;
}
} catch (e) { }
}
// Older versions have an update.locale file on disk.
let file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
file.append("update.locale");
this._readFileToString(file, function onRead1(error, content) {
if (!error) {
if (content[content.length - 1] == "\n") {
content = content.slice(0, -1);
}
this._locale = content;
cb();
return;
}
this._log.warn("update.locale not found in XCurProcD");
file = Services.dirsvc.get("GreD", Ci.nsIFile);
file.append("update.locale");
this._readFileToString(file, function onRead2(error, content) {
if (error) {
this._log.warn("update.locale not found in GreD");
this._log.error("Unable to obtain locale!");
this._locale = null;
cb();
return;
}
if (content[content.length - 1] == "\n") {
content = content.slice(0, -1);
}
this._locale = content;
cb();
}.bind(this));
}.bind(this));
},
_getInstallersJSON: function () {
let uri = Services.io.newURI(this.INSTALLERS_URI, null, null);
let channel = Services.io.newChannelFromURI(uri);
let is = channel.open();
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(is);
let data = sis.readBytes(is.available());
return JSON.parse(data);
},
/**
* Perform tasks during shutdown or during add-on disabling.
*/
shutdown: function () {
this._log.warn("Performing shutdown actions.");
if (this._retryTimer) {
this._retryTimer.cancel();
this._retryTimer = null;
}
Services.wm.removeListener(windowListener);
browserWindowCall(removeCSS);
},
/**
* Perform cleanup actions on uninstall.
*/
_cleanup: function (reason, cb) {
if (this._destroyed) {
return;
}
this._log.warn("Performing cleanup actions.");
this.shutdown();
if (!this._s.uninstallReason) {
this._s.uninstallReason = reason;
}
// Restoring the homepage override url is a little funky.
// If there was a custom pref before the hotfix executed, it will be saved
// in the backup pref. If not, there will be no backup pref.
//
// We ensure there is no custom value in the pref then we restore the
// original custom value, if any.
let defaultPrefs = Services.prefs.getDefaultBranch("startup.");
Services.prefs.setCharPref("startup.homepage_override_url",
defaultPrefs.getCharPref("homepage_override_url"));
if (this._prefs.prefHasUserValue("override_url_backup")) {
Services.prefs.setCharPref("startup.homepage_override_url",
this._prefs.getCharPref("override_url_backup"));
}
// Operates on user and default despite using a user branch object.
this._prefs.deleteBranch("");
this._saveState();
// The file log handle may prevent directory removal. Stop logging to
// a file.
if (gFileAppender) {
this._log.warn("Closing file appender.");
try {
gFileAppender.close();
} catch (e) {
this._log.warn("Error closing file appender: " + e);
}
this._log.removeAppender(gFileAppender);
gFileAppender = null;
}
if (this._stateDir && this._stateDir.exists()) {
try {
this._stateDir.remove(true);
} catch (ex) {
this._log.warn("Error removing state directory. Oh well.");
}
}
this._destroyed = true;
if (cb) {
cb();
}
},
/**
* Perform a self-initiated uninstall.
*
* This will cleanup and uninstall in the cleanest manner possible.
*
* We upload forensic state, cleanup, and finally uninstall the add-on.
*/
_selfUninstall: function (reason) {
this._log.warn("Performing a self-uninstall.");
this._uploadForensics(function () {
this._cleanup(reason || "UNKNOWN", uninstallHotfix);
}.bind(this));
},
/**
* Called when an uninstall is requested via the add-on manager.
*/
addonManagerUninstall: function () {
this._cleanup("ADDON_MANAGER_UNINSTALL");
},
/**
* Loads persisted state from the filesystem.
*
* The callback receives an object with various properties.
*
* If the state file does not exist, a default object is returned.
*/
_loadState: function (isInstall, cb) {
let state = {
// Old application version that we upgraded from.
// null if an upgrade hasn't been performed.
upgradedFrom: null,
// Reason the hotfix was uninstalled.
uninstallReason: null,
// Entity ID used to resume interrupted downloads.
_entityID: null,
// ID to attach to uploaded documents.
forensicsID: this._generateUUID(),
// Whether an installation is in progress.
// This may be true on start if Firefox was closed while an install
// was ongoing.
_installInProgress: false,
// Whether this client was at one time a candidate for the hotfix.
_everCompatible: false,
// The Windows version reported to GetVersionEx()/
reportedWindowsVersion: null,
// The actual Windows version, as detected by API sniffing. This will
// be different from reportedWindowsVersion if running in compatibility
// mode. The value may be under-reported. See _getActualWindowsVersion().
actualWindowsVersion: null,
// The first day an upgraded notification was presented.
firstNotifyDay: 0,
// The last day we presented an upgrade notification.
lastNotifyDay: 0,
// The number of times we've attempted a download.
downloadAttempts: 0,
// The number of completed downloads that didn't validate.
downloadFailures: 0,
// The number of times we've attempted to run the installer.
installAttempts: 0,
// The number of times the installer has completed successfully.
installSuccesses: 0,
// The number of times we've failed to start the launcher process.
installLauncherFailures: 0,
// The number of times we've launched the installer with failure.
installFailures: 0,
// The number of times we've shown a pop-up notification.
notificationsShown: 0,
// The number of times the user clicked the notification and started
// an install.
notificationsClicked: 0,
// The number of times the notification was dismissed.
notificationsDismissed: 0,
// The number of times the notfications was removed.
notificationsRemoved: 0,
// Maps launcher numeric exit codes to counts.
launcherExitCodes: {},
};
if (isInstall) {
this._log.warn("Fresh install. Loading fresh state.");
this._saveState();
cb(state);
return;
}
if (!this._stateFile.exists()) {
this._log.warn("No state file. First run?");
this._saveState();
cb(state);
return;
}
this._readFileToString(this._stateFile, function onRead(error, s) {
if (error) {
this._log.warn("Error reading state. Resetting.");
cb(state);
return;
}
try {
state = JSON.parse(s);
} catch (e) {
this._log.error("Error parsing JSON from state file: " + e);
}
cb(state);
}.bind(this));
},
_saveState: function (cb) {
// Many calls to this function don't wait on the callback. This can
// result in race conditions during saving. To prevent this, we chain
// calls if necessary.
this._pendingStateQueue.push(cb);
if (this._pendingStateQueue.length > 1) {
this._log.warn("Deferring save because another in progress.");
return;
}
// When we're done saving, call the callback for this instance. If we
// have chained saves, invoke them.
let onSave = function (error) {
this._log.warn("State save finished.");
let cb = this._pendingStateQueue.shift();
if (cb) {
try {
cb(error);
} catch (e) {}
}
if (this._pendingStateQueue.length) {
this._log.warn("Saving state file from chain.");
this._writeStringToFile(JSON.stringify(this._s), this._stateFile, onSave);
}
}.bind(this);
this._log.warn("Saving state file.");
this._writeStringToFile(JSON.stringify(this._s), this._stateFile, onSave);
},
/**
* Whether this hotfix is applicable to this Firefox install.
*/
_isHotfixApplicable: function () {
// Don't attempt upgrade past a specific modern version.
if (Services.vc.compare(Services.appinfo.version, this.MAX_UPGRADE_VERSION) >= 0) {
this._log.warn("Not applicable - modern version: " + Services.appinfo.version);
return false;
}
// We only target Windows.
if (Services.appinfo.OS != "WINNT") {
this._log.warn("Not applicable - not Windows: " + Services.appinfo.OS);
return false;
}
// Mozilla has only ever distributed 32-bit builds on Windows.
if (Services.appinfo.XPCOMABI != "x86-msvc") {
this._log.warn("Not applicable - not 32-bit: " + Services.appinfo.XPCOMABI);
return false;
}
// We don't upgrade Windows XP SP1 and older because they are only
// compatible up to Firefox 12.
let version = this._getWindowsVersion();
if (!version) {
// This is being conservative. If we see high incidence of this in the
// wild, we should first try to fix the underlying error and then
// consider ignoring the failure.
this._log.warn("Not applicable - unable to obtain Windows version.");
return false;
}
this._s.reportedWindowsVersion = version;
let actualVersion = this._getActualWindowsVersion();
// actualVersion could be [null, null, null] for very old Windows.
if (actualVersion && actualVersion[0]) {
this._s.actualWindowsVersion = actualVersion;
if (actualVersion[0] > version[0] || actualVersion[1] > version[1]) {
this._log.warn("Windows compatibility mode detected!");
}
this._log.warn("Reported Windows version: " + version[0] +
"." + version[1] + " SP" + version[2]);
this._log.warn("Actual Windows version: " + actualVersion[0] +
"." + actualVersion[1] + " SP" + actualVersion[2]);
version = actualVersion;
}
// 5.0 = 2000
// 5.1 = XP
// 5.2 = Server 2003, XP 64
if (version[0] < 5) {
this._log.warn("Not applicable - Running ancient Windows.");
return false;
}
if (version[0] == 5) {
if (version[1] == 0) {
this._log.warn("Not applicable - Running Windows 2000.");
return false;
} else if (version[1] == 2) {
// Server 2003 and XP 64 should not be used much in the wild. Just
// ignore them.
this._log.warn("Not applicable - Running Windows XP 64 or Server 2003.");
return false;
}
// We must be running XP (5.1).
// We require XP SP2 or higher on Firefox 13 and newer (bug 563318).
if (version[2] < 2) {
this._log.warn("Not applicable - Running Windows XP SP1 or older.");
return false;
}
}
// Partner builds may have undefined behavior when the full installer
// is executed.
try {
let partners = Services.prefs.getChildList("app.partner.");
if (partners.length) {
this._log.warn("Not applicable - app.partner.* pref set");
return false;
}
} catch (e) {}
// We are upgrading to release builds, so only target the release channel.
try {
let channel = Services.prefs.getCharPref("app.update.channel");
if (channel != "release") {
this._log.warn("Not applicable - not release channel: " + channel);
return false;
}
} catch (e) {}
// Only target installs that have updating enabled.
try {
if (!Services.prefs.getBoolPref("app.update.enabled")) {
this._log.warn("Not applicable - app.update disabled");
return false;
}
if (!Services.prefs.getBoolPref("app.update.auto")) {
this._log.warn("Not applicable - app.update.auto disabled");
return false;
}
} catch (e) {}
this._log.warn("Hotfix applicable");
return true;
},
/**
* Whether it appears we have upgraded to the version we were supposed to.
*/
_isUpgraded: function () {
return Services.vc.compare(Services.appinfo.version, this.UPGRADED_VERSION) >= 0;
},
// ---------------------------
// BEGIN EVENT MANAGEMENT CODE
// ---------------------------
/**
* Attempt to download and update Firefox.
*
* This is the main function that should be called to trigger download,
* launching the installer, and notifying the user. It is called
* automatically on startup and periodically after that if an error occurs.
*/
tryToDownloadAndUpdate: function () {
this._ensureInstallerDownloaded(function onResult(error) {
if (!error) {
this._onInstallerReady();
return;
}
this._log.warn("Download failed. Attempt " + this._s.downloadAttempts);
if (this._s.downloadFailures >= this.MAX_DOWNLOAD_FAILURES) {
this._log.warn("Reached maximum download failures. Giving up.");
this._displayFailureSupport();
this._selfUninstall("MAX_DOWNLOAD_FAILURES");
return;
}
this._retryTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
this._retryTimer.initWithCallback(function onTimer() {
this._log.warn("Retry timer fired. Attempting another download and/or update.");
this._retryTimer = null;
this.tryToDownloadAndUpdate();
}.bind(this), this.DOWNLOAD_RETRY_MILLISECONDS, this._retryTimer.TYPE_ONE_SHOT);
}.bind(this));
},
/**
* Called when an installer is downloaded and ready for execution.
*
* This may get called on startup.
*/
_onInstallerReady: function () {
this._log.warn("onInstallerReady()");
// If the download just finished and we haven't tried to run it yet, do
// so now without any warning.
if (!this._s.installAttempts) {
this._log.warn("Installer ready and no install attempts. Attempting " +
"now.");
this._attemptInstall(function onAttempt() {
this._log.warn("Install attempt failed in a possibly transient way.");
this._retryTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
this._retryTimer.initWithCallback(function onTimer() {
this._log.warn("Retry timer fired. Attempting another install attempt.");
this._retryTimer = null;
// We could call _onInstallerReady(). But it's safer to start from
// the beginning.
this.tryToDownloadAndUpdate();
}.bind(this), this.INSTALL_RETRY_MILLISECONDS, Ci.nsITimer.TYPE_ONE_SHOT);
}.bind(this));
return;
}
// After we've attempted an install once, show a notification and prompt
// to begin the install for all subsequent attempts.
// If we haven't notified yet, do it now.
if (!this._s.firstNotifyDay || !this._s.lastNotifyDay) {
this._log.warn("Showing notification because we haven't notified yet.");
this._showNotification();
return;
}
// We only show the notification at most once per day.
if (this._daysSinceEpoch() == this._s.lastNotifyDay) {
this._log.warn("Not showing notification because we've already shown today.");
return;
}
this._log.warn("Showing notification because we haven't notified today.");
this._showNotification();
},
/**
* Attempt to run the installer.
*
* The passed callback will only be called if the installer failed in a
* (hopefully) transient way. All other results are success or fatal error
* and result in the add-on being uninstalled.
*/
_attemptInstall: function (cb) {
this._s._installInProgress = true;
// Record our old version in a persisted location so we can later
// obtain info on hotfix-impacted users.
this._s.upgradedFrom = Services.appinfo.version;
this._saveState();
this._runInstaller(function onInstaller(error, status) {
this._s._installInProgress = false;
this._saveState();
// We have two types of errors: launcher process failed and installer
// failed.
// If the launcher process failed to even start, we assume it will fail
// again. So, we give up. If this assumption is wrong, we'll have a
// forensic ping with details of the failure so we can hopefully push
// a new hotfix with the fix.
if (error) {
this._log.error("Got error running launcher. Assuming fatal: " + error);
this._displayFailureSupport();
this._selfUninstall("LAUNCHER_START_ERROR");
return;
}
// In the case of successful install, Firefox will continue running.
// The update will be complete when Firefox is restarted. At that
// time, this add-on will detect it is no longer applicable and
// it will uninstall itself.
if (status == this.INSTALLER_STATUS_SUCCESS) {
this._log.warn("Installer finished successfully!");
Services.prefs.setCharPref("startup.homepage_override_url",
this.UPGRADE_HOMEPAGE_OVERRIDE);
// Set this here so it won't get overwritten during uninstall after
// restart.
this._s.uninstallReason = "SUCCESSFUL_UPGRADE";
this._saveState();
return;
}
if (status == this.INSTALLER_STATUS_FATAL) {
this._log.error("Error with install. Assuming non-recoverable.");
this._displayFailureSupport();
this._selfUninstall("LAUNCHER_RUN_ERROR");
return;
}
if (status == this.INSTALLER_STATUS_TRANSIENT) {
this._uploadForensics(function onForensics() {
if (cb) {
cb();
}
}.bind(this));
return;
}
// This should only happen due to a bug in the hotfix. We don't
// display the failure page here because the burden to fix this should
// be on the hotfix, not the user.
this._log.error("Unexpected installer status: " + status);
this._selfUninstall("UNEXPECTED_INSTALLER_STATUS");
}.bind(this));
},
/**
* Called when a the pop-up notification is shown.
*/
_onNotifyShown: function () {
this._log.warn("Notification shown.");
this._s.notificationsShown++;
let thisDay = this._daysSinceEpoch();
if (!this._s.firstNotifyDay) {
this._log.warn("This was the first notification.");
this._s.firstNotifyDay = thisDay;
}
this._s.lastNotifyDay = thisDay;
this._saveState();
},
/**
* Called when the "use now" button is clicked and we should start the upgrade.
*/
_onStartInstallClicked: function () {
this._log.warn("User clicked notification to begin install.");
this._s.notificationsClicked++;
this._saveState();
this._attemptInstall();
},
_onNotifyDismissed: function () {
this._log.warn("Notification dismissed.");
this._s.notificationsDismissed++;
this._saveState();
},
_onNotifyRemoved: function () {
this._log.warn("Notification removed.");
this._s.notificationsRemoved++;
this._saveState();
},
/**
* Called after a non-transient failure to run the installer.
*/
_displayFailureSupport: function () {
let window = Services.wm.getMostRecentWindow("navigator:browser");
let url = Services.urlFormatter.formatURL(this.FAILED_INSTALL_URL);
window.gBrowser.addTab(url);
},
/**
* This is called after an upgrade is performed using the hotfix.
*
* The role of this function is to try to send data to Mozilla and then
* to uninstall the hotfix.
*
* This isn't as easy as it sounds.
*
* For data upload to work, the user needs to agree to it. This requires
* Telemetry or FHR to be enabled. Telemetry has been around for ages, but
* it likely isn't enabled. FHR was introduced in Firefox 21. FHR, since
* it is enabled more easily, will likely be the vehicle that allows upload
* to occur.
*
* If we were to look for FHR state immediately after upgrade, chances are
* it won't be present because FHR waits 24 hours after first run before it
* does anything. This would mean that upload wouldn't be allowed and we'd
* likely not report data for pre-21 clients impacted by this hotfix. That's
* not acceptable.
*/
_onPostUpgrade: function () {
// If we know an upload is allowed, go ahead and do it without involving
// all the FHR logic.
if (this._isForensicsUploadAllowed()) {
this._selfUninstall("SUCCESSFUL_UPGRADE");
return;
}
// Since we've upgraded, we should be running on a build that has FHR.
// That means we can use its API.
try {
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
let reporter = service.healthReporter;
let policy = service.policy;
if (!reporter) {
this._log.warn("FHR not present.");
this._selfUninstall("SUCCESSFUL_UPGRADE");
return;
}
if (!policy) {
this._log.warn("FHR policy not present.");
this._selfUninstall("SUCCESSFUL_UPGRADE");
return;
}
// Ensure FHR is initialized.
reporter.onInit().then(
function onFHRInit() {
try{
// If the user has responded to the policy one way or another,
// do uninstall, maybe doing upload along the way.
if (policy.ensureNotifyResponse(policy.now())) {
this._log.warn("User has responded to FHR policy.");
this._selfUninstall("SUCCESSFUL_UPGRADE");
return;
}
// If the user hasn't responded, we wait. We could involve
// polling logic, etc here. By why add complexity? We just
// shut down and try again on the next browser restart.
this._log.warn("User hasn't responded to FHR policy. Shutting down.");
this.shutdown();
} catch (e) {
this._log.warn("Error interacting with FHR policy: " + e);
this._selfUninstall("SUCCESSFUL_UPGRADE");
}
}.bind(this),
function onFHRFailure() {
this._log.warn("Could not initialize FHR. Weird.");
this._selfUninstall("SUCCESSFUL_UPGRADE");
}.bind(this)
);
} catch (e) {
this._log.error("Exception interacting with FHR: " + e);
this._selfUninstall("SUCCESSFUL_UPGRADE");
}
},
// ----------------------
// BEGIN DOWNLOADING CODE
// ----------------------
/**
* Ensure the installer is downloaded and validated.
*
* If the file exists locally, it will be validated and used if it passes
* checks. If it doesn't pass checks, it will be removed and re-downloaded.
*
* If the file does not exist locally, it will be downloaded.
*/
_ensureInstallerDownloaded: function (cb) {
if (this._installerFile.exists()) {
this._log.warn("Existing installer present. Verifying.");
// NOTE: This could happen on startup, triggering up to 30MB in read I/O.
this._verifyDownload(this._installerFile, function onVerify(success) {
if (success) {
cb(null, this._installerFile);
return;
}
// If the existing file could not be validated, blow it away and
// try again.
// NOTE: we should never get here because the file will be validated
// before it is moved into its final location. In theory filesystem
// corruption could cause this failure, so we catch it.
this._log.warn("Removing downloaded file and entityID.");
this._installerFile.remove(false);
this._s._entityID = null;
this._saveState();
this._ensureInstallerDownloaded(cb);
}.bind(this));
return;
}
this._s.downloadAttempts++;
this._log.warn("Starting download. Attempt " + this._s.downloadAttempts);
this._saveState();
// No existing installer file. Download it.
this._doDownload(function onDownload(error) {
if (error) {
this._log.warn("Download did not complete successfully.");
cb(error);
return;
}
this._log.warn("Verifying download.");
this._verifyDownload(this._installerTempFile, function onResult(success) {
if (success) {
this._installerTempFile.moveTo(this._stateDir, this._installerFile.leafName);
this._log.warn("Moved installer to final location.");
this._s._entityID = null;
this._saveState();
cb(null, this._installerFile);
return;
}
this._s.downloadFailures++;
this._saveState();
this._log.warn("Download failure #" + this._s.downloadFailures);
this._log.warn("Removing temp file due to failed verification.");
this._installerTempFile.remove(false);
this._s._entityID = null;
this._saveState();
cb(new Error("Resetting download state because file verification failed."));
}.bind(this));
}.bind(this));
},
/**
* Download the installer.
*
* This will attempt to download this._installerURL to
* this._installerTempFile. If the download is already in progress
* (denoted by the existence of the file), we attempt to resume it.
*
* NOTE: a success to the cb does not necessarily mean the file can be
* trusted. Callers are responsible for validating the downloaded file.
*
* @param cb
* (function) Invoked on download completion. Receives an error
* indicator as its argument.
*/
_doDownload: function (cb) {
// We start with an nsIChannel that performs the HTTP GET. We take
// data from this channel and write it to a pipe - which is acting
// as an in-memory buffer. We create an nsIFileOutputStream to write
// to a temporary file. A nsISimpleStreamListener moves the data from
// the channel to the pipe on a background thread. A nsIAsyncStreamCopier
// moves the data from the pipe to the file on a background thread.
let channel = NetUtil.newChannel(NetUtil.newURI(this._installerURL))
.QueryInterface(Ci.nsIRequest)
.QueryInterface(Ci.nsIHttpChannel);
// Don't cache download because we do file resuming automatically.
channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
// Don't add cookies, etc.
channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
let resumeFromBytes = 0;
// Work around nsIFile caching, just in case.
this._installerTempFile = this._installerTempFile.clone();
if (this._installerTempFile.exists()) {
let size = this._installerTempFile.fileSize;
// If size >= expected, chances are the server will fail the range
// request. Verification will catch the size mismatch. It doesn't
// make sense to introduce complexity here.
if (size >= this._installerExpectedSize) {
this._log.warn("Temp file already matches expected size. " +
"Skipping download.");
cb(null);
return;
}
// If we have a partial download and we can resume, try to resume.
if (channel instanceof Ci.nsIResumableChannel && this._s._entityID) {
this._log.warn("Resuming download at byte offset " + size);
channel.resumeAt(size, this._s._entityID);
resumeFromBytes = size;
}
}
let lastReported = 0;
channel.notificationCallbacks = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
Ci.nsIChannelEventSink,
Ci.nsIProgressEventSink]),
// nsIInterfaceRequestor
getInterface: function (iid) {
return this.QueryInterface(iid);
},
// nsIProgressEventSink
// Log download progress.
// This arguably isn't necessary. But it helps with forensics.
onProgress: function (request, context, progress, progressMax) {
if (progressMax == -1) {
return;
}
let currentBytes = resumeFromBytes + progress;
let totalBytes = resumeFromBytes + progressMax;
if (currentBytes - lastReported < 1000000) {
return;
}
this._log.warn("Download progress: " + currentBytes + "/" + totalBytes);
lastReported = currentBytes;
}.bind(this),
onStatus: function () {},
// nsIChannelEventSink.
// We allow redirects to occur, but only if they are internal or to
// the same URI. The download URLs should never issue HTTP 3xx.
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
this._log.warn("Channel redirected!");
try {
newChannel.QueryInterface(Ci.nsIHttpChannel);
} catch (e) {
this._log.warn("New channel is not an nsIHttpChannel.");
throw Cr.NS_ERROR_NO_INTERFACE;
}
let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL);
let isSameURI = newChannel.URI.equals(oldChannel.URI);
this._log.warn("Redirect is internal? " + isInternal + "; is same URI? " +
isSameURI);
if (!isInternal || !isSameURI) {
this._log.warn("Cancelling channel redirect.");
oldChannel.cancel(Cr.NS_ERROR_ABORT);
throw Cr.NS_BINDING_REDIRECTED;
}
channel = newChannel;
callback.onRedirectVerifyCallback(Cr.NS_OK);
}.bind(this),
};
let pipe = Cc["@mozilla.org/pipe;1"]
.createInstance(Ci.nsIPipe);
// Bug 943511 removed the 5th argument. But it's required by older
// versions.
pipe.init(true, true, 0, 0xffffffff, null);
let copier = Cc["@mozilla.org/network/async-stream-copier;1"]
.createInstance(Ci.nsIAsyncStreamCopier);
let channelListener = Cc["@mozilla.org/network/simple-stream-listener;1"]
.createInstance(Ci.nsISimpleStreamListener);
let copierStarted = false;
let channelFailed = false;
channelListener.init(pipe.outputStream, {
onStartRequest: function (request, context) {
this._log.warn("channel:onStartRequest()");
try {
request.QueryInterface(Ci.nsIHttpChannel);
} catch (e) {
this._log.error("Unexpected error: channel isn't a nsIHttpChannel.");
request.cancel(Cr.NS_BINDING_ABORTED);
return;
}
try {
// Blocked by Windows parental controls.
if (request.responseStatus == 450) {
this._log.warn("Blocked by parental controls.");
request.cancel(Cr.NS_BINDING_ABORTED);
return;
}
} catch (e) {
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
this._log.warn("HTTP request seems to have failed.");
// We should see onStopRequest immediately. No need to cancel
// the request.
return;
}
}
// If we are attempting a resume and the entity ID changed, it
// isn't safe to continue the resume.
//
// The entity ID should never change for the installers. Encountering
// a change is very suspect of something funky happening, such as
// a captive portal or other proxy.
//
// We have a few choices here.
//
// We could blow away our partial file and try again. The risk here is
// that the condition is temporary (such as a captive portal) and we'll
// be making the client do more download work.
//
// We could pause for a little bit and try again later. This would
// introduce more state complexity.
//
// For now, we go with blowing away the partial download.
//
// NOTE: Firefox <27 don't verify the 206 response size is what we
// actually expect. This could lead to more failures than expected on
// those clients.
if (request.status == Cr.NS_ERROR_ENTITY_CHANGED) {
this._log.warn("Entity ID changed!");
this._s._entityID = null;
this._saveState();
resumeFromBytes = 0;
}
// Ensure a requested resume will actually work.
if (request instanceof Ci.nsIResumableChannel) {
let entityID = null;
try {
// This may throw.
entityID = request.entityID;
this._log.warn("Download is resumable.");
} catch (ex if ex instanceof Components.Exception &&
ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
resumeFromBytes = 0;
this._log.warn("Download is not resumable.");
} finally {
this._s._entityID = entityID;
this._saveState();
}
}
let openFlags = 0x02; // write
if (resumeFromBytes) {
openFlags |= 0x10; // append
} else {
openFlags |= 0x08 | 0x20; // create | truncate
}
// NOTE: safe-file-output-stream is buggy in append mode (bug 834042).
let fos = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
fos.init(this._installerTempFile, openFlags, 493 /* 0755 */, fos.DEFER_OPEN);
// We delay initializing the copier until this point because we don't
// want the copier mucking about with the file output stream before
// that stream has been seeked.
copier.init(pipe.inputStream, fos, null, true, false, 1048576, true, true);
copier.asyncCopy({
onStartRequest: function (request, context) {
this._log.warn("copier:onStartRequest()");
}.bind(this),
onStopRequest: function (request, context, status) {
this._log.warn("copier:onStopRequest(" + this._err2str(status) + ")");
if (channelFailed) {
this._log.warn("Channel didn't complete successfully.");
cb(new Error("Download failed."));
return;
}
if (!Components.isSuccessCode(status)) {
this._log.warn("File saving didn't complete successfully.");
cb(new Error("Download failed."));
return;
}
cb(null);
}.bind(this),
}, null); // copier.asyncCopy
copierStarted = true;
}.bind(this), // channelListener.onStartRequest
onStopRequest: function (request, context, status) {
this._log.warn("channel:onStopRequest(" + this._err2str(status) + ")");
let failChannel = function () {
if (channelFailed) {
return;
}
// We could be called before copier:onStartRequest. This would make
// the .cancel() below no-op. We set a flag to supplement
// the copier's status.
channelFailed = true;
// This should trigger the copier's onStopRequest(), which will
// invoke cb.
pipe.outputStream.closeWithStatus(Cr.NS_BINDING_ABORTED);
if (copierStarted) {
this._log.warn("Closing pipe due to failed channel.");
} else {
// But if we didn't start the copier, we need to call cb here.
this._log.warn("Channel failed before data was received.");
cb(new Error("Channel failed before data was received."));
}
}.bind(this);
if (!Components.isSuccessCode(status)) {
failChannel();
return;
}
let responseCode = channel.responseStatus;
this._log.warn("Got HTTP " + responseCode);
// 200=OK; 206=Partial Content
if (responseCode != 200 && responseCode != 206) {
failChannel();
return;
}
// This should trigger the copier's onStopRequest().
pipe.outputStream.close();
}.bind(this),
});
// NOTE: channelListener:onStopRequest will get called on shutdown, so
// we don't need to listen for shutdown events.
channel.asyncOpen(channelListener, null);
},
/**
* Verify a download matches expectations.
*
* @param file
* (nsIFile) Downloaded file to verify.
* @param cb
* (function) Receives a single boolean argument defining whether
* verification was successful.
*/
_verifyDownload: function (file, cb) {
// nsIFile.fileSize may lie on Windows. We copy the nsIFile as a workaround.
// See bug 1022704.
file = file.clone();
if (file.fileSize != this._installerExpectedSize) {
this._log.warn("File size does not match: " + file.fileSize + " != " +
this._installerExpectedSize);
cb(false);
return;
}
let hash = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hash.initWithString("SHA512");
let fis = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, fis.DEFER_OPEN);
let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
.createInstance(Ci.nsIInputStreamPump);
pump.init(fis, -1, -1, 0, 0, true);
pump.asyncRead({
onStartRequest: function () {},
onDataAvailable: function (request, context, is, offset, count) {
hash.updateFromStream(is, count);
}.bind(this),
onStopRequest: function (request, context, status) {
let digest = hash.finish(false);
digest = this._bin2hex(digest);
if (digest == this._installerExpectedHash) {
this._log.warn("File hash matches.");
cb(true);
return;
}
this._log.warn("File hash mismatch!");
cb(false);
return;
}.bind(this),
}, null);
},
// --------------------
// BEGIN INSTALLER CODE
// --------------------
/**
* Run the installer.
*
* This configures the launcher and attempts to run it.
*/
_runInstaller: function (cb) {
this._copyLauncherToFilesystem(function onCopy(error) {
if (error) {
cb(error, null);
return;
}
this._writeInstallerIniFile(function onIniWrite(error) {
if (error) {
cb(error, null);
return;
}
this._runLauncher(function onRun(error, status) {
cb(error, status);
}.bind(this));
}.bind(this));
}.bind(this));
},
/**
* Copy the launcher to the filesystem.
*
* The launcher is bundled with the extension inside the .xpi. We can't
* run exe files inside zip files, so we copy the file to the filesystem.
*/
_copyLauncherToFilesystem: function (cb) {
this._log.warn("Copying launcher exe to filesystem");
let sourceURI = Services.io.newURI(this.LAUNCHER_URI, null, null);
NetUtil.asyncFetch(sourceURI, function onFetch(is, status, request) {
if (!Components.isSuccessCode(status)) {
this._log.warn("Could not fetch launcher: " + this._err2str(status));
cb(new Error("Could not fetch launcher"));
return;
}
let fos = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
fos.init(this._launcherFile,
0x02 | 0x08 | 0x20, // write | create | truncate
493, // 0755
fos.DEFER_OPEN);
NetUtil.asyncCopy(is, fos, function onCopy(status) {
this._log.warn("Launcher copy finished - " + this._err2str(status));
if (!Components.isSuccessCode(status)) {
cb(new Error("Failed to copy launcher."));
return;
}
cb(null);
}.bind(this));
}.bind(this));
},
/**
* Determine whether the install path is writable.
*/
_isInstallPathWritable: function () {
// Ideally we'd test for writability of firefox.exe. However, Windows
// has almost certainly opened firefox.exe without write sharing access
// and attempts to open firefox.exe will likely result in
// ERROR_SHARING_VIOLATION. So, we choose an arbitrary should-not-exist
// path.
let file = this._targetDir.clone();
file.append("update-hotfix-test");
let fos = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
try {
fos.init(file,
FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND,
FileUtils.PERMS_FILE, 0);
fos.close();
file.remove(false);
return true;
} catch (e) {
this._log.warn("Could not open install directory for writing: " + e);
return false;
}
},
_writeInstallerIniFile: function (cb) {
const lines = [
"[Install]",
"PreventRebootRequired=true",
"QuickLaunchShortcut=false",
"DesktopShortcut=false",
"StartMenuShortcuts=false",
"MaintenanceService=true",
];
// Adding InstallDirectoryPath to the ini makes the installer run hidden.
// We want the installer to run hidden if it can (fewer things that could
// go wrong, etc).
//
// The only time we can't run hidden is on Windows XP when the install
// directory is not writable. On modern Windows versions, we'll request
// UAC elevation, which should result in a writable installation directory.
let v = this._s.actualWindowsVersion;
let installWritable = this._isInstallPathWritable();
if (installWritable) {
this._log.warn("Install path is writable.");
} else {
this._log.warn("Install path is not writable.");
}
if (v[0] != 5 || v[1] != 1 || installWritable) {
this._log.warn("Launching installer in hidden mode.");
lines.push("InstallDirectoryPath=%InstallPath%");
}
let template = lines.join("\n");
let content = template.replace("%InstallPath%", this._targetDir.path);
this._writeStringToFile(content, this._installerIniFile, cb);
},
_getFailureDetailsFromLog: function (cb) {
this._readFileToString(this._launcherLogFile, function onData(error, s) {
if (error) {
cb(error);
return;
}
let lines = s.split("\n");
if (!lines.length) {
cb(null, null);
return;
}
let o = {status: lines[0], details: null};
try {
o.status = parseInt(o.status, 10);
} catch (e) {
this._log.warn("Unable to parse status code to int: " + o.status);
}
if (lines.length > 1) {
o.details = lines[1];
}
cb(null, o);
}.bind(this));
},
LAUNCHER_EXIT_CODES: {
0: "Success",
1: "InitializationFailed",
2: "InvalidArguments",
3: "CantWriteToInstallDir",
4: "OpenLauncherLogFailed",
5: "ElevationFailed",
6: "ElevationCancelled",
7: "UnarchivingFailed",
8: "AccessingLogFailed",
9: "InstallationFailed",
100: "Unknown",
},
LAUNCHER_TRANSIENT_EXIT_CODES: [5, 6],
// Installer completed successfully.
INSTALLER_STATUS_SUCCESS: "success",
// Installer failed but in a transient manner.
INSTALLER_STATUS_TRANSIENT: "transient",
// Installer failed in a manner that will likely reproduce.
INSTALLER_STATUS_FATAL: "fatal",
/**
* Run the launcher and attempt to install Firefox.
*
* The callback receives (error, installStatus). error will be defined
* if we got something unexpected in JavaScript land. This should not
* happen! installStatus will be one of the INSTALLER_STATUS_*
* prototype entries.
*/
_runLauncher: function (cb) {
this._s.installAttempts++;
this._saveState();
let process = Cc["@mozilla.org/process/util;1"]
.createInstance(Ci.nsIProcess);
process.init(this._launcherFile);
let args = [
this._installerFile.path,
this._installerIniFile.path,
this._targetDir.path,
this._launcherLogFile.path,
];
let onProcessFinished = function (process, topic) {
if (topic == "process-failed") {
this._log.warn("Launcher failed.");
this._s.installLauncherFailures++;
this._saveState();
cb(new Error("Launcher failed."));
return;
}
if (topic != "process-finished") {
this._log.warn("Unexpected topic for process observer: " + topic);
// Fall through.
} else {
this._log.warn("Got process-finished.");
}
// nsIProcess has a... fun API. If the process failed to even start,
// it sends "process-finished" and sets a non-zero process.exitValue.
// If the process exits with a non-0 code, process.exitValue is
// defined. However, if the process exits successfully, .exitValue
// is undefined. Furthermore, .exitValue appears to sometimes be
// undefined on non-0 exit code. Oy.
this._getFailureDetailsFromLog(function (error, details) {
if (error) {
this._log.warn("Could not read launcher log file.");
this._s.installFailures++;
this._saveState();
cb(new Error("Launcher likely failed."));
return;
}
let exitCode = process.exitValue;
if (exitCode === undefined) {
this._log.warn("Got undefined exitValue.");
exitCode = details.status;
// We should never get this. Just in case, we set a dummy value.
if (exitCode === null) {
exitCode = 100;
}
}
let exitStatus = this.LAUNCHER_EXIT_CODES[exitCode] || "unknown";
this._log.warn("Launcher exit code: " + exitCode + "; status: " + exitStatus);
this._s.launcherExitCodes[exitCode] = (this._s.launcherExitCodes[exitCode] || 0) + 1;
if (exitCode) {
this._log.warn("Launcher did not complete successfully.");
this._s.installFailures++;
this._saveState();
if (details.details) {
this._log.warn("Failure details: " + details.details);
}
if (this.LAUNCHER_TRANSIENT_EXIT_CODES.indexOf(exitCode) !== -1) {
cb(null, this.INSTALLER_STATUS_TRANSIENT);
} else {
cb(null, this.INSTALLER_STATUS_FATAL);
}
return;
}
this._s.installSuccesses++;
this._saveState();
cb(null, this.INSTALLER_STATUS_SUCCESS);
}.bind(this));
}.bind(this);
this._log.warn("Running launcher process.");
process.runAsync(args, args.length, {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsISupportsWeakReference,
Ci.nsISupports,
Ci.nsIObserver,
]),
observe: function (process, topic, data) {
onProcessFinished(process, topic);
},
});
},
// ------------------------------
// BEGIN NOTIFICATION AND UI CODE
// ------------------------------
_showNotification: function () {
let bundle = Services.strings.createBundle("chrome://firefox-hotfix/locale/hotfix.properties");
let message = bundle.GetStringFromName("ready_full");
let primaryMessage = bundle.GetStringFromName("install_full");
// We initially display a somewhat generic message. After a week passes,
// we move on to more descriptive messages.
let delta = this._daysSinceEpoch() - this._s.firstNotifyDay;
if (this._s.firstNotifyDay && delta >= 7) {
let index = Math.floor(Math.random() * 2) + 1;
message = bundle.GetStringFromName("ready_alt" + index);
}
let mainAction = {
label: primaryMessage,
accessKey: bundle.GetStringFromName("install.accesskey"),
callback: this._onStartInstallClicked.bind(this),
};
// We don't need to set because "Not Now" is the default message and it
// should already be translated.
let secondaryActions = null;
// Dismissing the notification to the tray doesn't work in Firefox <15 due
// to incompatibilities between our CSS/XBL and legacy versions of the
// pop-up notifications feature. See bug 1031021.
let anchorID = "upgrade-notification-tray-icon";
let removeOnDismissal = false;
if (Services.vc.compare(Services.appinfo.version, "14.*") <= 0) {
anchorID = null;
removeOnDismissal = true;
}
let us = this;
let options = {
// Persist forever. We avoid really large values because it may overflow
// JavaScript's Date type. 2^36 in milliseconds corresponds to ~795 days.
timeout: Math.pow(2, 36),
// Keep showing during location changes. Upgrades are important!
persistWhileVisible: true,
removeOnDismissal: removeOnDismissal,
eventCallback: function (action) {
switch (action) {
case "dismissed":
us._onNotifyDismissed();
return;
case "removed":
us._onNotifyRemoved();
return;
case "shown":
us._onNotifyShown();
return;
}
},
};
let window = Services.wm.getMostRecentWindow("navigator:browser");
if (window.switchToTabHavingURI(this.NEW_TAB_URL, true)) {
this._log.warn("Showing notification on existing tab.");
} else {
this._log.warn("Opening a new tab to show notification.");
}
try {
window.PopupNotifications.show(window.gBrowser.selectedBrowser, "upgrade",
message, anchorID, mainAction,
secondaryActions, options);
} catch (e) {
this._log.error("Error showing notification: " + e);
}
},
// -----------------------------
// BEGIN FORENSIC UPLOADING CODE
// -----------------------------
/**
* Obtain the forensic payload to be uploaded to the server for analyis.
*
* NOTE: It is important that we don't leak private or user identifiable
* data in the payload!
*/
_getForensicPayload: function (cb) {
let o = {
os: Services.appinfo.OS,
xpcomabi: Services.appinfo.XPCOMABI,
version: Services.appinfo.version,
state: {},
};
// _ prefixed keys in state are local.
if (this._s) {
for (let key in this._s) {
if (key[0] == "_") {
continue;
}
o.state[key] = this._s[key];
}
}
o.locale = this._locale;
// this._installerURL won't be defined if hotfix not applicable.
try {
o.installerURL = this._installerURL.spec;
} catch (e) {
o.installerURL = null;
}
// Getting windows version will fail on not windows.
try {
let version = this._getWindowsVersion();
o.windowsMajorVersion = version[0];
o.windowsServicePackVersion = version[1];
} catch (e) {
o.windowsMajorVersion = null;
o.windowsServicePackVersion = null;
}
try {
o.partner = Services.prefs.getCharPref("app.partner");
} catch (e) {
o.partner = null;
}
try {
o.channel = Services.prefs.getCharPref("app.update.channel");
} catch (e) {
o.channel = null;
}
try {
o.updateEnabled = Services.prefs.getBoolPref("app.update.enabled");
} catch (e) {
o.updateEnabled = null;
}
try {
o.updateAuto = Services.prefs.getBoolPref("app.update.auto");
} catch (e) {
o.updateAuto = null;
}
let populateLog = function (file, key, isJSON, isUTF16, cb) {
// We sometimes don't even have the file objects if we upload
// very early.
if (!file) {
cb();
return;
}
this._readFileToString(file, function onRead(error, data) {
if (error) {
this._log.error("Error fetching log: " + key);
o[key] = null;
cb();
return;
}
if (isUTF16) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-16LE";
data = converter.ConvertToUnicode(data);
}
try {
data = this._removeSensitiveData(data);
if (isJSON) {
try {
o[key] = [];
let lines = data.split("\n");
for each (let line in lines) {
if (line) {
o[key].push(JSON.parse(line));
}
}
} catch (e) {
o[key].push([Date.now(), 40, "ERROR READING LOG"]);
throw e;
}
} else {
// Normalize line endings.
data = data.replace("\r\n", "\n", "g");
o[key] = data;
}
} catch (e) {
this._log.error("Error populating log: " + e);
}
cb();
}.bind(this));
}.bind(this);
populateLog(this._logFile, "logHotfix", true, false, function () {
populateLog(this._installerLogFile, "logInstaller", false, true, function () {
cb(o);
}.bind(this));
}.bind(this));
},
/**
* Whether forensics upload is allowed.
*
* Forensics upload is only allowed if the client was applicable for the
* hotfix *and* the user has agreed to data collection.
*
* We look at both the Telemetry and FHR controls for data upload to
* determine the answer to whether the user has agreed to data collection.
*/
_isForensicsUploadAllowed: function () {
if (!this._s._everCompatible) {
this._log.warn("Forensics upload not allowed because hotfix not compatible.");
return false;
}
try {
if (Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
this._log.warn("Forensics upload allowed via Telemetry.");
return true;
}
} catch (e) {}
try {
const FHR_PREFS = [
"datareporting.policy.dataSubmissionPolicyAccepted",
"datareporting.healthreport.uploadEnabled",
];
for each (let pref in FHR_PREFS) {
if (!Services.prefs.getBoolPref(pref)) {
return false;
}
}
this._log.warn("Forensics upload allowed via FHR.");
return true;
} catch (e) {}
// We couldn't find a user intent to allow uploading. Disallow.
this._log.warn("Forensics upload not allowed due to missing user agreement.");
return false;
},
/**
* Upload forensic data to Mozilla. Maybe.
*
* This will attempt to upload forensic data collected by this hotfix to
* Mozilla. If upload is not applicable or not allowed (via the user not
* agreeing to data collection), then this no-ops.
*
* All uploaded data should be anonymous and contain no personally
* identifiable information.
*/
_uploadForensics: function (cb) {
if (!this._isForensicsUploadAllowed()) {
cb(null);
return;
}
this._log.warn("Starting forensic upload procedure.");
if (gFileAppender) {
try {
gFileAppender.flush();
} catch (ex) {
this._log.warn("Error flushing file appender: " + ex);
}
}
this._getForensicPayload(function onPayload(o) {
this._log.warn("Obtained payload.");
let url = this.UPLOAD_URL + this._s.forensicsID;
this._uploadJSON(url, o, function onUpload(error) {
if (error) {
this._log.warn("Upload error: " + error);
cb(error);
return;
}
this._log.warn("Upload success.");
cb(null);
}.bind(this));
}.bind(this));
},
/**
* Attempt to remove sensitive data from a string that we don't want sent
* to the server.
*/
_removeSensitiveData: function (s) {
const DIR_SERVICE_KEYS = {
ProfD: "Profile",
CurWorkDir: "CurWorkDir",
Home: "Home",
TmpD: "Temp",
ProgF: "ProgramFiles",
};
let doReplacement = function (name, file) {
let uri = Services.io.newFileURI(file);
// Order of operation is important here. We do the URI before the path
// version because the path may be a subset of the URI.
// try here because .spec may throw.
try {
// We use .spec and not .asciiSpec because some paths may have
// Unicode.
s = s.replace(uri.spec, '<' + name + 'URI>', 'g');
} catch (e) { }
s = s.replace(file.path, '<' + name + 'Path>', 'g');
};
// This might be a superset of CurWorkDir so do first.
try {
doReplacement("InstallDir", this._targetDir);
} catch (e) { }
for (let key in DIR_SERVICE_KEYS) {
try {
let name = DIR_SERVICE_KEYS[key];
let file = Services.dirsvc.get(key, Ci.nsIFile);
doReplacement(name, file);
} catch (e) { }
}
return s;
},
// --------------------------
// BEGIN GENERIC UTILITY CODE
// --------------------------
/**
* Number of days elapsed since UNIX epoch.
*
* This is used to drive prompting. We never prompt twice on the same
* calendar day (in local time). Furthermore, we select an arbitrary time
* in the early morning when most people should be asleep. This avoids
* a potentially dual prompt between 23:59 and 00:00.
*/
_daysSinceEpoch: function () {
let now = new Date();
// New days begin at 4 AM local time.
let adjusted = now.getTime() - now.getTimezoneOffset() + 4 * 60 * 60 * 1000;
return Math.floor(adjusted / MILLISECONDS_IN_DAY);
},
/**
* Convert a binary string to its hex representation.
*/
_bin2hex: function (b) {
let result = "";
for (let i = 0; i < b.length; i++) {
let hex = b.charCodeAt(i).toString(16);
if (hex.length == 1) {
hex = "0" + hex;
}
result += hex;
}
return result;
},
/**
* Attempt to resolve an integer error code to a string name.
*/
_err2str: function (e) {
for (let k in Cr) {
if (Cr[k] == e) {
return k;
}
}
return "" + e;
},
/**
* Read a file to a string buffer.
*
* @param file
* (nsIFile) File to read.
* @param cb
* (function) Receives (error, string). The string will be in
* binary.
*/
_readFileToString: function (file, cb) {
let fis = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
try {
fis.init(file, 0x01 /* read */, 292 /* 0444 */, fis.DEFER_OPEN);
} catch (ex) {
this._log.warn("Error reading file: " + ex);
cb(ex);
return;
}
let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
.createInstance(Ci.nsIInputStreamPump);
pump.init(fis, -1, -1, 0, 0, true);
let bis = Cc["@mozilla.org/binaryinputstream;1"]
.createInstance(Ci.nsIBinaryInputStream);
let buffer = [];
pump.asyncRead({
onStartRequest: function () {},
onDataAvailable: function (request, context, is, offset, count) {
bis.setInputStream(is);
buffer.push(bis.readBytes(is.available()));
},
onStopRequest: function (request, context, status) {
if (!Components.isSuccessCode(status)) {
this._log.warn("Error reading file " + file.leafName + ": " +
this._err2str(status));
cb(new Error("Error reading file."));
return;
}
cb(null, buffer.join(""));
}.bind(this),
}, null);
},
/**
* Write a string to a file off the main thread.
*
* @param s
* (string) Data to write. We assume it is unicode.
* @param file
* (nsIFile) File to write to.
* @param cb
* (function) Called on completion. Receives (error).
*/
_writeStringToFile: function (s, file, cb) {
let os = FileUtils.openSafeFileOutputStream(file);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let is = converter.convertToInputStream(s);
NetUtil.asyncCopy(is, os, function (status) {
if (!Components.isSuccessCode(status)) {
this._log.warn("Error writing file " + file.leafName + ": " +
this._err2str(status));
cb(new Error("Error writing to file."));
return;
}
cb(null);
}.bind(this));
},
/**
* Read an input stream synchronously and strip trailing newline.
*/
_readInputStream: function (is) {
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(is);
let text = sis.read(sis.available());
sis.close();
if (text[text.length - 1] == "\n") {
text = text.slice(0, -1);
}
return text;
},
_getKernel32: function () {
try {
return ctypes.open("Kernel32");
} catch (e) {
this._log.error("Error opening Kernel32 with ctypes: " + e);
}
return null;
},
/**
* Obtain the version of Windows being used.
*
* Returns an array of [Major Version, Minor Version, Service Pack Version].
* If we couldn't obtain data, returns null.
*/
_getWindowsVersion: function () {
const BYTE = ctypes.uint8_t;
const WORD = ctypes.uint16_t;
const DWORD = ctypes.uint32_t;
const WCHAR = ctypes.jschar;
const BOOL = ctypes.int;
// This structure is described at:
// http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
const SZCSDVERSIONLENGTH = 128;
const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', [
{dwOSVersionInfoSize: DWORD},
{dwMajorVersion: DWORD},
{dwMinorVersion: DWORD},
{dwBuildNumber: DWORD},
{dwPlatformId: DWORD},
{szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
{wServicePackMajor: WORD},
{wServicePackMinor: WORD},
{wSuiteMask: WORD},
{wProductType: BYTE},
{wReserved: BYTE},
]);
let kernel32 = this._getKernel32();
if (!kernel32) {
this._log.error("Could not obtain Kernel32 ctypes wrapper.");
return null;
}
try {
let GetVersionEx = kernel32.declare("GetVersionExW",
ctypes.winapi_abi,
BOOL,
OSVERSIONINFOEXW.ptr);
let struct = OSVERSIONINFOEXW();
struct.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
if (!GetVersionEx(struct.address())) {
this._log.error("GetVersionExW call failed.");
return null;
}
return [struct.dwMajorVersion, struct.dwMinorVersion, struct.wServicePackMajor];
} catch (e) {
this._log.error("Exception calling GetVersionExW: " + e);
} finally {
kernel32.close();
}
},
/**
* Attempts to determine the actual running Windows version.
*
* GetVersionExW() lies when the application is running in Windows
* compatibility mode. This function falls back to an alternate version
* detection method to identify clients that are running in compatibility
* mode.
*
* It does this by looking for exported symbols from Windows libraries.
* If a symbol is present, we are pretty much guaranteed that it is a modern
* version.
*
* This function returns an array of [majorVersion, minorVersion, spVersion].
* The actual versions could be greater than what is returned. However,
* we will never give back versions higher than what's possible. Therefore
* this function is useful for enforcing a lower bound, not an upper bound.
*
* This function doesn't work for ancient versions of Windows. Also,
* reporting of Service Pack version is not highly accurate. It may be
* null if it can't be determined.
*/
_getActualWindowsVersion: function () {
const HMODULE = ctypes.size_t;
const LPCSTR = ctypes.char.ptr;
const LPCTSTR = ctypes.jschar.ptr;
const FARPROC = ctypes.size_t;
let kernel32 = this._getKernel32();
if (!kernel32) {
return null;
}
try {
let GetModuleHandleW = kernel32.declare("GetModuleHandleW",
ctypes.winapi_abi,
HMODULE,
LPCTSTR);
let module = GetModuleHandleW("kernel32");
if (!module) {
this._log.warn("Unable to obtain handle on kernel32.dll.");
return null;
}
let GetProcAddress = kernel32.declare("GetProcAddress",
ctypes.winapi_abi,
FARPROC,
HMODULE,
LPCSTR);
let isFunctionExported = function (m, f) {
let NOFUNCTION = ctypes.UInt64(0);
let result = GetProcAddress(m, f);
return ctypes.UInt64.compare(result, NOFUNCTION) != 0;
}.bind(this);
// 4.10 Windows 98 and 98 SE
// 4.90 Windows ME
// 5.0 Windows 2000 original, SP1 to SP4
// 5.1 Windows XP original, SP1 to SP3
// 5.2 Server 2003 original, SP1 and SP2; XP 64
// 6.0 Windows Vista original, SP1 and SP2; Server 2008
// 6.1 Windows 7 original, SP1; Server 2008 R2 original and SP1
// 6.2 Windows 8; Server 2012
// 6.3 Windows 8.1; Server 2012 R2
let majorVersion = null;
let minorVersion = null;
let spMajorVersion = null;
// CheckTokenCompatibility() was added in Windows 8 and Server 2012.
// http://msdn.microsoft.com/en-us/library/windows/desktop/hh448477%28v=vs.85%29.aspx
if (isFunctionExported(module, "CheckTokenCompatibility")) {
majorVersion = 6;
minorVersion = 2;
}
// GetNumaNodeNumberFromHandle() was added in Windows 7 and Server 2008 R2.
// http://msdn.microsoft.com/en-us/library/dd405492%28v=vs.85%29.aspx
else if (isFunctionExported(module, "GetNumaNodeNumberFromHandle")) {
majorVersion = 6;
minorVersion = 1;
// CopyContext() was added in 7 SP1 and Server 2008 R2 SP1.
// http://msdn.microsoft.com/en-us/library/windows/desktop/hh134234%28v=vs.85%29.aspx
if (isFunctionExported(module, "CopyContext")) {
spMajorVersion = 1;
} else {
spMajorVersion = 0;
}
}
// GetLocaleInfoEx() was added in Vista and Server 2008.
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd318103%28v=vs.85%29.aspx
else if (isFunctionExported(module, "GetLocaleInfoEx")) {
majorVersion = 6;
minorVersion = 0;
// NOTE: We could not find any kernel32 APIs added in Vista SP2.
// GetPhysicallyInstalledSystemMemory() was added in Vista SP1 and
// Server 2008.
// http://msdn.microsoft.com/en-us/library/windows/desktop/cc300158%28v=vs.85%29.aspx
if (isFunctionExported(module, "GetPhysicallyInstalledSystemMemory")) {
spMajorVersion = 1;
} else {
spMajorVersion = 0;
}
}
// FindFirstStreamW() was added in Vista and Server 2003.
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364424%28v=vs.85%29.aspx
else if (isFunctionExported(module, "FindFirstStreamW")) {
majorVersion = 5;
minorVersion = 2;
}
// GetNativeSystemInfo() was added in XP and Server 2003.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms724340%28v=vs.85%29.aspx
else if (isFunctionExported(module, "GetNativeSystemInfo")) {
majorVersion = 5;
minorVersion = 1;
// GetLogicalProcessorInformation() was added in XP SP3, XP 64, and Vista.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683194%28v=vs.85%29.aspx
if (isFunctionExported(module, "GetLogicalProcessorInformation")) {
spMajorVersion = 3;
}
// DecodeSystemPointer() was added in Vista, XP SP2, Server 2008, and
// Server 2003 SP1.
// http://msdn.microsoft.com/en-us/library/bb432243%28v=vs.85%29.aspx
else if (isFunctionExported(module, "DecodeSystemPointer")) {
spMajorVersion = 2;
}
// GetDllDirectory() was added in XP SP1, Vista, and Server 2003.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683186%28v=vs.85%29.aspx
else if (isFunctionExported(module, "GetDllDirectory")) {
spMajorVersion = 1;
}
// We must be XP original.
else {
spMajorVersion = 0;
}
}
return [majorVersion, minorVersion, spMajorVersion];
} catch (e) {
this._log.warn("Exception obtaining true Windows version: " + e);
return null;
} finally {
kernel32.close();
}
},
_convertString: function (input, outputEncoding) {
let scs = Cc["@mozilla.org/streamConverters;1"]
.getService(Ci.nsIStreamConverterService);
let loader = Cc["@mozilla.org/network/stream-loader;1"]
.createInstance(Ci.nsIStreamLoader);
let buffer = "";
loader.init({
onStreamComplete: function (loader, context, status, length, result) {
buffer = String.fromCharCode.apply(this, result);
},
});
let converter = scs.asyncConvertData("uncompressed", outputEncoding,
loader, null);
let ss = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
ss.data = input;
converter.onStartRequest(null, null);
converter.onDataAvailable(null, null, ss, 0, input.length);
converter.onStopRequest(null, null, null);
return buffer;
},
/**
* Upload an object to a server.
*/
_uploadJSON: function (url, obj, cb) {
let json = JSON.stringify(obj);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let utf8Payload = converter.ConvertFromUnicode(json);
utf8Payload += converter.Finish();
let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
payloadStream.data = this._convertString(utf8Payload, "gzip");
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.mozBackgroundRequest = true;
request.open("POST", url, true);
request.overrideMimeType("text/plain");
request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
request.setRequestHeader("Content-Encoding", "gzip");
request.addEventListener("error", function onError(event) {
cb(event);
}.bind(this));
request.addEventListener("load", function onLoad() {
cb(null);
}.bind(this));
this._log.warn("Uploading payload to " + url);
request.send(payloadStream);
},
_generateUUID: function () {
let uuid = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
.generateUUID()
.toString();
// Trim {}.
return uuid.substring(1, uuid.length - 1);
},
};
// Our singleton instance.
this.manager = new UpgradeManager();
this.uninstallHotfix = function () {
log.warn("Uninstalling add-on.");
AddonManager.getAddonByID(manager.addonID, function(addon) {
if (addon) {
addon.uninstall();
}
});
};
/**
* Call a function for each browser window.
*/
function browserWindowCall(cb) {
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let win = e.getNext().QueryInterface(Ci.nsIDOMWindow);
if (win) {
cb(win);
}
}
}
let windowListener = {
onOpenWindow: function (window) {
let dw = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
dw.addEventListener("load", function loadListener() {
dw.removeEventListener("load", loadListener, false);
injectCSS(dw);
});
},
onCloseWindow: function () {},
onWindowTitleChange: function () {},
};
/**
* Inject CSS to register our custom notification type.
*
* Old Firefox versions don't have icons in notifications. We add
* that. We also give our notification a dedicated binding to make
* manipulation easier.
*/
function injectCSS(window) {
let doc = window.document;
let pi = doc.createProcessingInstruction(
"xml-stylesheet",
'id="upgrade-notification-css" href="chrome://firefox-hotfix/content/notification.css" type="text/css"');
doc.insertBefore(pi, doc.getElementById('main-window'));
let image = doc.createElement("image");
image.setAttribute("id", "upgrade-notification-tray-icon");
image.setAttribute("class", "notification-anchor-icon");
image.setAttribute("role", "button");
let el = doc.getElementById("notification-popup-box");
if (el) {
el.appendChild(image);
}
}
function removeCSS(window) {
let nodes = window.document.childNodes;
let toDelete = [];
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
if (node.nodeName != "xml-stylesheet") {
continue;
}
if (node.data.indexOf('id="upgrade-notification-css"') != -1) {
toDelete.push(node);
}
}
for each (let node in toDelete) {
node.parentNode.removeChild(node);
}
let el = window.document.getElementById("upgrade-notification-tray-icon");
if (el) {
el.parentNode.removeChild(el);
}
}