/* -*- 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 (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 URI_UNINSTALL_PROPERTIES = "chrome://branding/content/uninstall.properties"; const KEY_APPDIR = "XCurProcD"; const KEY_COMPONENTS_DIR = "ComsD"; const KEY_PLUGINS_DIR = "APlugns"; const KEY_EXECUTABLE_FILE = "XREExeF"; // see prio.h const PR_RDONLY = 0x01; const PR_WRONLY = 0x02; const PR_APPEND = 0x10; const nsIWindowsRegKey = Components.interfaces.nsIWindowsRegKey; //----------------------------------------------------------------------------- /** * Console logging support */ function LOG(s) { dump("*** PostUpdateWin: " + s + "\n"); } /** * 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); } /** * Return the full path given a relative path and a base directory. */ function getFileRelativeTo(dir, relPath) { var file = dir.clone().QueryInterface(Components.interfaces.nsILocalFile); file.setRelativeDescriptor(dir, relPath); return file; } /** * 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; } /** * Gets the current value of the locale. It's possible for this preference to * be localized, so we have to do a little extra work here. Similar code * exists in nsHttpHandler.cpp when building the UA string. */ function getLocale() { const prefName = "general.useragent.locale"; var prefs = Components.classes["@mozilla.org/preferences-service;1"]. getService(Components.interfaces.nsIPrefBranch); try { return prefs.getComplexValue(prefName, Components.interfaces.nsIPrefLocalizedString).data; } catch (e) {} return prefs.getCharPref(prefName); } //----------------------------------------------------------------------------- const PREFIX_INSTALLING = "installing: "; const PREFIX_CREATE_FOLDER = "create folder: "; const PREFIX_CREATE_REGISTRY_KEY = "create registry key: "; const PREFIX_STORE_REGISTRY_VALUE_STRING = "store registry value string: "; 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 + "\n"; this._outputStream.write(s, s.length); }, /** * This function creates an empty install wizard log file and returns * a reference to the resulting nsIFile. */ _getInstallLogFile: function() { function makeLeafName(index) { return "install_wizard" + index + ".log" } var file = getFile(KEY_APPDIR); file.append("uninstall"); if (!file.exists()) return null; file.append("x"); var n = 0; do { file.leafName = makeLeafName(++n); } while (file.exists()); if (n == 1) return null; // What, no install wizard log file? file.leafName = makeLeafName(n - 1); 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() { var file = getFile(KEY_APPDIR); file.append("updates"); file.append("0"); file.append("update.log"); if (file.exists()) return file; file = getFile(KEY_APPDIR); file.append("updates"); file.append("last-update.log"); if (file.exists()) return file; return null; }, /** * Read update.log to extract information about files that were * newly added for this update. */ _readUpdateLog: function(logFile, entries) { var appDir = getFile(KEY_APPDIR); var stream; try { stream = openFileInputStream(logFile). QueryInterface(Components.interfaces.nsILineInputStream); var dirs = {}; var line = {}; while (stream.readLine(line)) { var data = line.value.split(" "); if (data[0] == "EXECUTE" && data[1] == "ADD") { var file = getFileRelativeTo(appDir, data[2]); // remember all parent directories var parent = file.parent; while (!parent.equals(appDir)) { dirs[parent.path] = true; parent = parent.parent; } // remember the file entries.files.push(file.path); } } for (var d in dirs) entries.dirs.push(d); // Sort the directories so that subdirectories are deleted first. entries.dirs.sort(); } finally { if (stream) stream.close(); } }, /** * This function initializes the log writer and is responsible for * translating 'update.log' to the install wizard format. */ begin: function() { var installLog = this._getInstallLogFile(); var updateLog = this._getUpdateLogFile(); if (!installLog || !updateLog) return; var entries = { dirs: [], files: [] }; this._readUpdateLog(updateLog, entries); this._outputStream = openFileOutputStream(installLog, PR_WRONLY | PR_APPEND); this._writeLine("\nUPDATE [" + new Date().toUTCString() + "]"); var i; // The log file is processed in reverse order, so list directories before // files to ensure that the directories will be deleted. for (i = 0; i < entries.dirs.length; ++i) this._writeLine(PREFIX_CREATE_FOLDER + entries.dirs[i]); for (i = 0; i < entries.files.length; ++i) this._writeLine(PREFIX_INSTALLING + entries.files[i]); }, /** * This function records the creation of a registry key. */ registryKeyCreated: function(keyPath) { if (!this._outputStream) return; this._writeLine(PREFIX_CREATE_REGISTRY_KEY + keyPath + " []"); }, /** * This function records the creation of a registry key value. */ registryKeyValueSet: function(keyPath, valueName) { if (!this._outputStream) return; this._writeLine( PREFIX_STORE_REGISTRY_VALUE_STRING + keyPath + " [" + valueName + "]"); }, end: function() { if (!this._outputStream) return; this._outputStream.close(); this._outputStream = null; } }; var installLogWriter; //----------------------------------------------------------------------------- /** * A thin wrapper around nsIWindowsRegKey that keeps track of its path * and notifies the installLogWriter when modifications are made. */ 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, ACCESS_WRITE: nsIWindowsRegKey.ACCESS_WRITE, ACCESS_ALL: nsIWindowsRegKey.ACCESS_ALL, ROOT_KEY_CURRENT_USER: nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, ROOT_KEY_LOCAL_MACHINE: nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, 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); }, createChild: function(path, mode) { var child = this._key.createChild(path, mode); var key = new RegKey(child, this._root, this._path + "\\" + path); if (installLogWriter) installLogWriter.registryKeyCreated(key.toString()); return key; }, readStringValue: function(name) { return this._key.readStringValue(name); }, writeStringValue: function(name, value) { this._key.writeStringValue(name, value); if (installLogWriter) installLogWriter.registryKeyValueSet(this.toString(), name); }, writeIntValue: function(name, value) { this._key.writeIntValue(name, value); if (installLogWriter) installLogWriter.registryKeyValueSet(this.toString(), 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); }, removeChild: function(name) { this._key.removeChild(name); }, toString: function() { var root; switch (this._root) { 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; } }; //----------------------------------------------------------------------------- /* RegKey.prototype = { // The name of the registry key name: ""; // An array of strings, where each even-indexed string names a value, // and the subsequent string provides data for the value. values: []; // An array of RegKey objects. children: []; } */ /** * This function creates a heirarchy of registry keys. If any of the * keys or values already exist, then they will be updated instead. * @param rootKey * The root registry key from which to create the new registry * keys. * @param data * A JS object with properties: "name", "values", and "children" * as defined above. All children of this key will be created. */ function createRegistryKeys(rootKey, data) { var key; try { key = rootKey.createChild(data.name, rootKey.ACCESS_WRITE); var i; if ("values" in data) { for (i = 0; i < data.values.length; i += 2) key.writeStringValue(data.values[i], data.values[i + 1]); } if ("children" in data) { for (i = 0; i < data.children.length; ++i) createRegistryKeys(key, data.children[i]); } key.close(); } catch (e) { LOG(e); if (key) key.close(); } } /** * This function deletes the specified registry key and optionally all of its * children. * @param rootKey * The parent nsIwindowRegKey of the key to delete. * @param name * The name of the key to delete. * @param recurse * Pass true to also delete all children of the named key. Take care! */ function deleteRegistryKey(rootKey, name, recurse) { if (!rootKey.hasChild(name)) { LOG("deleteRegistryKey: rootKey does not have child: \"" + name + "\""); return; } if (recurse) { var key = rootKey.openChild(name, rootKey.ACCESS_ALL); try { for (var i = key.childCount - 1; i >= 0; --i) deleteRegistryKey(key, key.getChildName(i), true); } finally { key.close(); } } rootKey.removeChild(name); } /** * This method walks the registry looking for the registry keys of * the previous version of the application. */ function locateOldInstall(key, ourInstallDir) { var result, 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"); var menuPath = mainKey.readStringValue("Program Folder Path"); mainKey.close(); if (newFile(installDir).equals(ourInstallDir)) { result = new Object(); result.fullName = childName; result.versionWithLocale = productVer; result.version = productVer.split(" ")[0]; result.menuPath = menuPath; } } productKey.close(); if (result) break; } } childKey.close(); if (result) break; } } catch (e) { result = null; if (childKey) childKey.close(); if (productKey) productKey.close(); if (mainKey) mainKey.close(); } return result; } /** * Delete registry keys left-over from the previous version of the app * installed at our location. */ function deleteOldRegKeys(key, info) { deleteRegistryKey(key, info.fullName + " " + info.version, true); var productKey = key.openChild(info.fullName, key.ACCESS_ALL); var productCount; try { deleteRegistryKey(productKey, info.versionWithLocale, true); productCount = productKey.childCount; } finally { productKey.close(); } if (productCount == 0) key.removeChild(info.fullName); } /** * The installer sets various registry keys and values that may need to be * updated. * * This operation is a bit tricky since we do not know the previous value of * brandFullName. As a result, we must walk the registry looking for an * existing key that references the same install directory. We assume that * the value of vendorShortName does not change across updates. */ function updateRegistry(rootKey) { LOG("updateRegistry"); var ourInstallDir = getFile(KEY_APPDIR); 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 brandFullName = brandBundle.GetStringFromName("brandFullName"); var vendorShortName; try { // The Thunderbird vendorShortName is "Mozilla Thunderbird", but we // just want "Thunderbird", so allow it to be overridden in prefs. var prefs = Components.classes["@mozilla.org/preferences-service;1"]. getService(Components.interfaces.nsIPrefBranch); vendorShortName = prefs.getCharPref("app.update.vendorName.override"); } catch (e) { vendorShortName = brandBundle.GetStringFromName("vendorShortName"); } var key = new RegKey(); var oldInstall; try { key.open(rootKey, "SOFTWARE\\" + vendorShortName, key.ACCESS_READ); oldInstall = locateOldInstall(key, ourInstallDir); } finally { key.close(); } if (!oldInstall) { LOG("no existing registry keys found"); return; } // Maybe nothing needs to be changed... if (oldInstall.fullName == brandFullName && oldInstall.version == app.version) { LOG("registry is up-to-date"); return; } // Delete the old keys: try { key.open(rootKey, "SOFTWARE\\" + vendorShortName, key.ACCESS_READ); deleteOldRegKeys(key, oldInstall); } finally { key.close(); } // Create the new keys: var versionWithLocale = app.version + " (" + getLocale() + ")"; var installPath = ourInstallDir.path + "\\"; var pathToExe = getFile(KEY_EXECUTABLE_FILE).path; var Key_bin = { name: "bin", values: [ "PathToExe", pathToExe ] }; var Key_extensions = { name: "Extensions", values: [ "Components", getFile(KEY_COMPONENTS_DIR).path + "\\", "Plugins", getFile(KEY_PLUGINS_DIR).path + "\\" ] }; var Key_nameWithVersion = { name: brandFullName + " " + app.version, values: [ "GeckoVer", app.platformVersion ], children: [ Key_bin, Key_extensions ] }; var Key_main = { name: "Main", values: [ "Install Directory", installPath, "PathToExe", pathToExe, "Program Folder Path", oldInstall.menuPath ] }; var Key_uninstall = { name: "Uninstall", values: [ "Description", brandFullName + " (" + app.version + ")", "Uninstall Log Folder", installPath + "uninstall" ] }; var Key_versionWithLocale = { name: versionWithLocale, children: [ Key_main, Key_uninstall ] }; var Key_name = { name: brandFullName, values: [ "CurrentVersion", versionWithLocale ], children: [ Key_versionWithLocale ] }; var Key_brand = { name: vendorShortName, children: [ Key_name, Key_nameWithVersion ] }; try { key.open(rootKey, "SOFTWARE", key.ACCESS_READ); createRegistryKeys(key, Key_brand); } finally { key.close(); } if (rootKey != RegKey.prototype.ROOT_KEY_LOCAL_MACHINE) return; // Now, do the same thing for the Add/Remove Programs control panel: const uninstallRoot = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; try { key.open(rootKey, uninstallRoot, key.ACCESS_READ); var oldName = oldInstall.fullName + " (" + oldInstall.version + ")"; deleteRegistryKey(key, oldName, false); } finally { key.close(); } var uninstallBundle = sbs.createBundle(URI_UNINSTALL_PROPERTIES); var nameWithVersion = brandFullName + " (" + app.version + ")"; var uninstaller = getFile(KEY_APPDIR); uninstaller.append("uninstall"); uninstaller.append("uninstall.exe"); var uninstallString = uninstaller.path + " /ua \"" + versionWithLocale + "\""; Key_uninstall = { name: nameWithVersion, values: [ "Comment", brandFullName, "DisplayIcon", pathToExe + ",0", // XXX don't hardcode me! "DisplayName", nameWithVersion, "DisplayVersion", versionWithLocale, "InstallLocation", ourInstallDir.path, // no trailing slash "Publisher", vendorShortName, "UninstallString", uninstallString, "URLInfoAbout", uninstallBundle.GetStringFromName("URLInfoAbout"), "URLUpdateInfo", uninstallBundle.GetStringFromName("URLUpdateInfo") ] }; var child; try { key.open(rootKey, uninstallRoot, key.ACCESS_READ); createRegistryKeys(key, Key_uninstall); // Create additional DWORD keys for NoModify and NoRepair: child = key.openChild(nameWithVersion, key.ACCESS_WRITE); child.writeIntValue("NoModify", 1); child.writeIntValue("NoRepair", 1); } finally { if (child) child.close(); key.close(); } } //----------------------------------------------------------------------------- function nsPostUpdateWin() { } 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() { try { // We use a global object here for the install log writer, so that the // registry updating code can easily inform it of registry keys that it // may create. installLogWriter = new InstallLogWriter(); try { installLogWriter.begin(); updateRegistry(RegKey.prototype.ROOT_KEY_CURRENT_USER); updateRegistry(RegKey.prototype.ROOT_KEY_LOCAL_MACHINE); } finally { installLogWriter.end(); installLogWriter = null; } } catch (e) { LOG(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; }