blob: d68c181fbfbb6792a11edec49394b00eae632317 [file] [log] [blame]
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Update Service.
*
* The Initial Developer of the Original Code is Google Inc.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Darin Fisher <darin@meer.net> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/**
* This file contains an implementation of nsIRunnable, which may be invoked
* to perform post-update modifications to the windows registry and uninstall
* logs required to complete an update of the application. This code is very
* specific to the xpinstall wizard for windows.
*/
const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
const KEY_APPDIR = "XCurProcD";
const KEY_TMPDIR = "TmpD";
const KEY_UPDROOT = "UpdRootD";
const KEY_UAPPDATA = "UAppData";
// see prio.h
const PR_RDONLY = 0x01;
const PR_WRONLY = 0x02;
const PR_APPEND = 0x10;
const PERMS_FILE = 0644;
const PERMS_DIR = 0700;
const nsIWindowsRegKey = Components.interfaces.nsIWindowsRegKey;
var gConsole = null;
var gAppUpdateLogPostUpdate = false;
//-----------------------------------------------------------------------------
/**
* Console logging support
*/
function LOG(s) {
if (gAppUpdateLogPostUpdate) {
dump("*** PostUpdateWin: " + s + "\n");
gConsole.logStringMessage(s);
}
}
/**
* This function queries the XPCOM directory service.
*/
function getFile(key) {
var dirSvc =
Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties);
return dirSvc.get(key, Components.interfaces.nsIFile);
}
/**
* Creates a new file object given a native file path.
* @param path
* The native file path.
* @return nsILocalFile object for the given native file path.
*/
function newFile(path) {
var file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
file.initWithPath(path);
return file;
}
/**
* This function returns a file input stream.
*/
function openFileInputStream(file) {
var stream =
Components.classes["@mozilla.org/network/file-input-stream;1"].
createInstance(Components.interfaces.nsIFileInputStream);
stream.init(file, PR_RDONLY, 0, 0);
return stream;
}
/**
* This function returns a file output stream.
*/
function openFileOutputStream(file, flags) {
var stream =
Components.classes["@mozilla.org/network/file-output-stream;1"].
createInstance(Components.interfaces.nsIFileOutputStream);
stream.init(file, flags, 0644, 0);
return stream;
}
//-----------------------------------------------------------------------------
const PREFIX_FILE = "File: ";
function InstallLogWriter() {
}
InstallLogWriter.prototype = {
_outputStream: null, // nsIOutputStream to the install wizard log file
/**
* Write a single line to the output stream.
*/
_writeLine: function(s) {
s = s + "\r\n";
this._outputStream.write(s, s.length);
},
/**
* This function creates an empty uninstall update log file if it doesn't
* exist and returns a reference to the resulting nsIFile.
*/
_getUninstallLogFile: function() {
var file = getFile(KEY_APPDIR);
file.append("uninstall");
if (!file.exists())
return null;
file.append("uninstall.log");
if (!file.exists())
file.create(Components.interfaces.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
return file;
},
/**
* Return the update.log file. Use last-update.log file in case the
* updates/0 directory has already been cleaned out (see bug 311302).
*/
_getUpdateLogFile: function() {
function appendUpdateLogPath(root) {
var file = root.clone();
file.append("updates");
file.append("0");
file.append("update.log");
if (file.exists())
return file;
file = root;
file.append("updates");
file.append("last-update.log");
if (file.exists())
return file;
return null;
}
// See the local appdata first if app dir is under Program Files.
var file = null;
var updRoot;
try {
updRoot = getFile(KEY_UPDROOT);
} catch (e) {
}
if (updRoot) {
file = appendUpdateLogPath(updRoot);
// When updating from Fx 2.0.0.1 to 2.0.0.3 (or later) on Vista,
// we will have to see also user app data (see bug 351949).
if (!file)
file = appendUpdateLogPath(getFile(KEY_UAPPDATA));
}
// See the app dir if not found or app dir is out of Program Files.
if (!file)
file = appendUpdateLogPath(getFile(KEY_APPDIR));
return file;
},
/**
* Read update.log to extract information about files that were
* newly added for this update.
*/
_readUpdateLog: function(logFile, entries) {
var stream;
try {
stream = openFileInputStream(logFile).
QueryInterface(Components.interfaces.nsILineInputStream);
var line = {};
while (stream.readLine(line)) {
var data = line.value.split(" ");
if (data[0] == "EXECUTE" && data[1] == "ADD") {
// The uninstaller requires the path separator to be "\" and
// relative paths to start with a "\".
var relPath = "\\" + data[2].replace(/\//g, "\\");
entries[relPath] = null;
}
}
} finally {
if (stream)
stream.close();
}
},
/**
* Read install_wizard log files to extract information about files that were
* previously added by the xpinstall installer and software update.
*/
_readXPInstallLog: function(logFile, entries) {
var stream;
try {
stream = openFileInputStream(logFile).
QueryInterface(Components.interfaces.nsILineInputStream);
function fixPath(path, offset) {
return path.substr(offset).replace(appDirPath, "");
}
var appDir = getFile(KEY_APPDIR);
var appDirPath = appDir.path;
var line = {};
while (stream.readLine(line)) {
var entry = line.value;
// This works with both the entries from xpinstall (e.g. Installing: )
// and from update (e.g. installing: )
var searchStr = "nstalling: ";
var index = entry.indexOf(searchStr);
if (index != -1) {
entries[fixPath(entry, index + searchStr.length)] = null;
continue;
}
searchStr = "Replacing: ";
index = entry.indexOf(searchStr);
if (index != -1) {
entries[fixPath(entry, index + searchStr.length)] = null;
continue;
}
searchStr = "Windows Shortcut: ";
index = entry.indexOf(searchStr);
if (index != -1) {
entries[fixPath(entry + ".lnk", index + searchStr.length)] = null;
continue;
}
}
} finally {
if (stream)
stream.close();
}
},
_readUninstallLog: function(logFile, entries) {
var stream;
try {
stream = openFileInputStream(logFile).
QueryInterface(Components.interfaces.nsILineInputStream);
var line = {};
var searchStr = "File: ";
while (stream.readLine(line)) {
var index = line.value.indexOf(searchStr);
if (index != -1) {
var str = line.value.substr(index + searchStr.length);
entries.push(str);
}
}
} finally {
if (stream)
stream.close();
}
},
/**
* This function initializes the log writer and is responsible for
* translating 'update.log' and the 'install_wizard' logs to the NSIS format.
*/
begin: function() {
var updateLog = this._getUpdateLogFile();
if (!updateLog)
return;
var newEntries = { };
this._readUpdateLog(updateLog, newEntries);
try {
const nsIDirectoryEnumerator = Components.interfaces.nsIDirectoryEnumerator;
const nsILocalFile = Components.interfaces.nsILocalFile;
var prefixWizLog = "install_wizard";
var uninstallDir = getFile(KEY_APPDIR);
uninstallDir.append("uninstall");
var entries = uninstallDir.directoryEntries.QueryInterface(nsIDirectoryEnumerator);
while (true) {
var wizLog = entries.nextFile;
if (!wizLog)
break;
if (wizLog instanceof nsILocalFile && !wizLog.isDirectory() &&
wizLog.leafName.indexOf(prefixWizLog) == 0) {
this._readXPInstallLog(wizLog, newEntries);
wizLog.remove(false);
}
}
}
catch (e) {}
if (entries)
entries.close();
var uninstallLog = this._getUninstallLogFile();
var oldEntries = [];
this._readUninstallLog(uninstallLog, oldEntries);
// Prevent writing duplicate entries in the log file
for (var relPath in newEntries) {
if (oldEntries.indexOf(relPath) != -1)
delete newEntries[relPath];
}
if (newEntries.length == 0)
return;
// since we are not running with elevated privs, we can't write out
// the log file (at least, not on Vista). So, write the output to
// temp, and then later, we'll pass the file (gCopiedLog) to
// the post update clean up process, which can copy it to
// the desired location (because it will have elevated privs)
gCopiedLog = getFile(KEY_TMPDIR);
gCopiedLog.append("uninstall");
gCopiedLog.createUnique(gCopiedLog.DIRECTORY_TYPE, PERMS_DIR);
if (uninstallLog)
uninstallLog.copyTo(gCopiedLog, "uninstall.log");
gCopiedLog.append("uninstall.log");
LOG("uninstallLog = " + uninstallLog.path);
LOG("copiedLog = " + gCopiedLog.path);
if (!gCopiedLog.exists())
gCopiedLog.create(Components.interfaces.nsILocalFile.NORMAL_FILE_TYPE,
PERMS_FILE);
this._outputStream =
openFileOutputStream(gCopiedLog, PR_WRONLY | PR_APPEND);
// The NSIS uninstaller deletes all directories where the installer has
// added a file if the directory is empty after the files have been removed
// so there is no need to log directories.
for (var relPath in newEntries)
this._writeLine(PREFIX_FILE + relPath);
},
end: function() {
if (!this._outputStream)
return;
this._outputStream.close();
this._outputStream = null;
}
};
var installLogWriter;
var gCopiedLog;
//-----------------------------------------------------------------------------
/**
* A thin wrapper around nsIWindowsRegKey
* note, only the "read" methods are exposed. If you want to write
* to the registry on Vista, you need to be a priveleged app.
* We've moved that code into the uninstaller.
*/
function RegKey() {
// Internally, we may pass parameters to this constructor.
if (arguments.length == 3) {
this._key = arguments[0];
this._root = arguments[1];
this._path = arguments[2];
} else {
this._key =
Components.classes["@mozilla.org/windows-registry-key;1"].
createInstance(nsIWindowsRegKey);
}
}
RegKey.prototype = {
_key: null,
_root: null,
_path: null,
ACCESS_READ: nsIWindowsRegKey.ACCESS_READ,
ROOT_KEY_CURRENT_USER: nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
ROOT_KEY_LOCAL_MACHINE: nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
ROOT_KEY_CLASSES_ROOT: nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
close: function() {
this._key.close();
this._root = null;
this._path = null;
},
open: function(rootKey, path, mode) {
this._key.open(rootKey, path, mode);
this._root = rootKey;
this._path = path;
},
openChild: function(path, mode) {
var child = this._key.openChild(path, mode);
return new RegKey(child, this._root, this._path + "\\" + path);
},
readStringValue: function(name) {
return this._key.readStringValue(name);
},
hasValue: function(name) {
return this._key.hasValue(name);
},
hasChild: function(name) {
return this._key.hasChild(name);
},
get childCount() {
return this._key.childCount;
},
getChildName: function(index) {
return this._key.getChildName(index);
},
toString: function() {
var root;
switch (this._root) {
case this.ROOT_KEY_CLASSES_ROOT:
root = "HKEY_KEY_CLASSES_ROOT";
break;
case this.ROOT_KEY_LOCAL_MACHINE:
root = "HKEY_LOCAL_MACHINE";
break;
case this.ROOT_KEY_CURRENT_USER:
root = "HKEY_CURRENT_USER";
break;
default:
LOG("unknown root key");
return "";
}
return root + "\\" + this._path;
}
};
/**
* This method walks the registry looking for the registry keys of
* the previous version of the application.
*/
function haveOldInstall(key, brandFullName, version) {
var ourInstallDir = getFile(KEY_APPDIR);
var result = false;
var childKey, productKey, mainKey;
try {
for (var i = 0; i < key.childCount; ++i) {
var childName = key.getChildName(i);
childKey = key.openChild(childName, key.ACCESS_READ);
if (childKey.hasValue("CurrentVersion")) {
for (var j = 0; j < childKey.childCount; ++j) {
var productVer = childKey.getChildName(j);
productKey = childKey.openChild(productVer, key.ACCESS_READ);
if (productKey.hasChild("Main")) {
mainKey = productKey.openChild("Main", key.ACCESS_READ);
var installDir = mainKey.readStringValue("Install Directory");
mainKey.close();
LOG("old install? " + installDir + " vs " + ourInstallDir.path);
LOG("old install? " + childName + " vs " + brandFullName);
LOG("old install? " + productVer.split(" ")[0] + " vs " + version);
if (newFile(installDir).equals(ourInstallDir) &&
(childName != brandFullName ||
productVer.split(" ")[0] != version)) {
result = true;
}
}
productKey.close();
if (result)
break;
}
}
childKey.close();
if (result)
break;
}
} catch (e) {
result = false;
if (childKey)
childKey.close();
if (productKey)
productKey.close();
if (mainKey)
mainKey.close();
}
return result;
}
function checkRegistry()
{
LOG("checkRegistry");
var result = false;
// Firefox is the only toolkit app that needs to do this.
// return false for other applications.
var app = Components.classes["@mozilla.org/xre/app-info;1"].
getService(Components.interfaces.nsIXULAppInfo);
if (app.name == "Firefox") {
try {
var key = new RegKey();
key.open(RegKey.prototype.ROOT_KEY_CLASSES_ROOT, "FirefoxHTML\\shell\\open\\command", key.ACCESS_READ);
var commandKey = key.readStringValue("");
LOG("commandKey = " + commandKey);
// if "-requestPending" is not found, we need to do the cleanup
result = (commandKey.indexOf("-requestPending") == -1);
} catch (e) {
LOG("failed to open command key for FirefoxHTML: " + e);
}
key.close();
}
return result;
}
function checkOldInstall(rootKey, vendorShortName, brandFullName, version)
{
var key = new RegKey();
var result = false;
try {
key.open(rootKey, "SOFTWARE\\" + vendorShortName, key.ACCESS_READ);
LOG("checkOldInstall: " + key + " " + brandFullName + " " + version);
result = haveOldInstall(key, brandFullName, version);
} catch (e) {
LOG("failed trying to find old install: " + e);
}
key.close();
return result;
}
//-----------------------------------------------------------------------------
function nsPostUpdateWin() {
gConsole = Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService);
var prefs = Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch);
try {
gAppUpdateLogPostUpdate = prefs.getBoolPref("app.update.log.all");
}
catch (ex) {
}
try {
if (!gAppUpdateLogPostUpdate)
gAppUpdateLogPostUpdate = prefs.getBoolPref("app.update.log.PostUpdate");
}
catch (ex) {
}
}
nsPostUpdateWin.prototype = {
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsIRunnable) ||
iid.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
run: function() {
// When uninstall/uninstall.update exists the uninstaller has already
// updated the uninstall.log with the files added by software update.
var updateUninstallFile = getFile(KEY_APPDIR);
updateUninstallFile.append("uninstall");
updateUninstallFile.append("uninstall.update");
if (updateUninstallFile.exists()) {
LOG("nothing to do, uninstall.log has already been updated");
return;
}
try {
installLogWriter = new InstallLogWriter();
try {
installLogWriter.begin();
} finally {
installLogWriter.end();
installLogWriter = null;
}
} catch (e) {
LOG(e);
}
var app =
Components.classes["@mozilla.org/xre/app-info;1"].
getService(Components.interfaces.nsIXULAppInfo).
QueryInterface(Components.interfaces.nsIXULRuntime);
var sbs =
Components.classes["@mozilla.org/intl/stringbundle;1"].
getService(Components.interfaces.nsIStringBundleService);
var brandBundle = sbs.createBundle(URI_BRAND_PROPERTIES);
var vendorShortName = brandBundle.GetStringFromName("vendorShortName");
var brandFullName = brandBundle.GetStringFromName("brandFullName");
if (!gCopiedLog &&
!checkRegistry() &&
!checkOldInstall(RegKey.prototype.ROOT_KEY_LOCAL_MACHINE,
vendorShortName, brandFullName, app.version) &&
!checkOldInstall(RegKey.prototype.ROOT_KEY_CURRENT_USER,
vendorShortName, brandFullName, app.version)) {
LOG("nothing to do, so don't launch the helper");
return;
}
try {
var winAppHelper =
app.QueryInterface(Components.interfaces.nsIWinAppHelper);
// note, gCopiedLog could be null
if (gCopiedLog)
LOG("calling postUpdate with: " + gCopiedLog.path);
else
LOG("calling postUpdate without a log");
winAppHelper.postUpdate(gCopiedLog);
} catch (e) {
LOG("failed to launch the helper to do the post update cleanup: " + e);
}
}
};
//-----------------------------------------------------------------------------
var gModule = {
registerSelf: function(compMgr, fileSpec, location, type) {
compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
for (var key in this._objects) {
var obj = this._objects[key];
compMgr.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
fileSpec, location, type);
}
},
getClassObject: function(compMgr, cid, iid) {
if (!iid.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
for (var key in this._objects) {
if (cid.equals(this._objects[key].CID))
return this._objects[key].factory;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
},
_makeFactory: #1= function(ctor) {
function ci(outer, iid) {
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
return (new ctor()).QueryInterface(iid);
}
return { createInstance: ci };
},
_objects: {
manager: { CID : Components.ID("{d15b970b-5472-40df-97e8-eb03a04baa82}"),
contractID : "@mozilla.org/updates/post-update;1",
className : "nsPostUpdateWin",
factory : #1#(nsPostUpdateWin)
},
},
canUnload: function(compMgr) {
return true;
}
};
function NSGetModule(compMgr, fileSpec) {
return gModule;
}