| /* -*- 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; |
| } |