michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir; michael@0: const APP_DATA_DIR = OS.Constants.Path.winAppDataDir; michael@0: michael@0: /************************************* michael@0: * Windows app installer michael@0: * michael@0: * The Windows installation process will generate the following files: michael@0: * michael@0: * ${FolderName} = sanitized app name + "-" + manifest url hash michael@0: * michael@0: * %APPDATA%/${FolderName} michael@0: * - webapp.ini michael@0: * - webapp.json michael@0: * - ${AppName}.exe michael@0: * - ${AppName}.lnk michael@0: * / uninstall michael@0: * - webapp-uninstaller.exe michael@0: * - shortcuts_log.ini michael@0: * - uninstall.log michael@0: * / chrome/icons/default/ michael@0: * - default.ico michael@0: * michael@0: * After the app runs for the first time, a profiles/ folder will also be michael@0: * created which will host the user profile for this app. michael@0: */ michael@0: michael@0: /** michael@0: * Constructor for the Windows native app shell michael@0: * michael@0: * @param aApp {Object} the app object provided to the install function michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aCategories {Array} array of app categories michael@0: * @param aRegistryDir {String} (optional) path to the registry michael@0: */ michael@0: function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { michael@0: CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); michael@0: michael@0: if (this.isPackaged) { michael@0: this.size = aApp.updateManifest.size / 1024; michael@0: } michael@0: michael@0: this.webapprt = this.appNameAsFilename + ".exe"; michael@0: this.configJson = "webapp.json"; michael@0: this.webappINI = "webapp.ini"; michael@0: this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico"); michael@0: this.uninstallDir = "uninstall"; michael@0: this.uninstallerFile = OS.Path.join(this.uninstallDir, michael@0: "webapp-uninstaller.exe"); michael@0: this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini"); michael@0: this.zipFile = "application.zip"; michael@0: michael@0: this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ]; michael@0: if (this.isPackaged) { michael@0: this.backupFiles.push(this.zipFile); michael@0: } michael@0: michael@0: this.uninstallSubkeyStr = this.uniqueName; michael@0: } michael@0: michael@0: NativeApp.prototype = { michael@0: __proto__: CommonNativeApp.prototype, michael@0: size: null, michael@0: michael@0: /** michael@0: * Creates a native installation of the web app in the OS michael@0: * michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aZipPath {String} path to the zip file for packaged apps (undefined michael@0: * for hosted apps) michael@0: */ michael@0: install: Task.async(function*(aManifest, aZipPath) { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: // If the application is already installed, this is a reinstallation. michael@0: if (WebappOSUtils.getInstallPath(this.app)) { michael@0: return yield this.prepareUpdate(aManifest, aZipPath); michael@0: } michael@0: michael@0: this._setData(aManifest); michael@0: michael@0: let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName); michael@0: michael@0: // Create a temporary installation directory. michael@0: let dir = getFile(TMP_DIR, this.uniqueName); michael@0: dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); michael@0: let tmpDir = dir.path; michael@0: michael@0: // Perform the installation in the temp directory. michael@0: try { michael@0: yield this._createDirectoryStructure(tmpDir); michael@0: yield this._getShortcutName(installDir); michael@0: yield this._copyWebapprt(tmpDir); michael@0: yield this._copyUninstaller(tmpDir); michael@0: yield this._createConfigFiles(tmpDir); michael@0: michael@0: if (aZipPath) { michael@0: yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); michael@0: } michael@0: michael@0: yield this._getIcon(tmpDir); michael@0: } catch (ex) { michael@0: yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: michael@0: // Apply the installation. michael@0: this._removeInstallation(true, installDir); michael@0: michael@0: try { michael@0: yield this._applyTempInstallation(tmpDir, installDir); michael@0: } catch (ex) { michael@0: this._removeInstallation(false, installDir); michael@0: yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Creates an update in a temporary directory to be applied later. michael@0: * michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aZipPath {String} path to the zip file for packaged apps (undefined michael@0: * for hosted apps) michael@0: */ michael@0: prepareUpdate: Task.async(function*(aManifest, aZipPath) { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: this._setData(aManifest); michael@0: michael@0: let installDir = WebappOSUtils.getInstallPath(this.app); michael@0: if (!installDir) { michael@0: throw ERR_NOT_INSTALLED; michael@0: } michael@0: michael@0: if (this.uniqueName != OS.Path.basename(installDir)) { michael@0: // Bug 919799: If the app is still in the registry, migrate its data to michael@0: // the new format. michael@0: throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; michael@0: } michael@0: michael@0: let updateDir = OS.Path.join(installDir, "update"); michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: yield OS.File.makeDir(updateDir); michael@0: michael@0: // Perform the update in the "update" subdirectory. michael@0: try { michael@0: yield this._createDirectoryStructure(updateDir); michael@0: yield this._getShortcutName(installDir); michael@0: yield this._copyUninstaller(updateDir); michael@0: yield this._createConfigFiles(updateDir); michael@0: michael@0: if (aZipPath) { michael@0: yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); michael@0: } michael@0: michael@0: yield this._getIcon(updateDir); michael@0: } catch (ex) { michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Applies an update. michael@0: */ michael@0: applyUpdate: Task.async(function*() { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: let installDir = WebappOSUtils.getInstallPath(this.app); michael@0: let updateDir = OS.Path.join(installDir, "update"); michael@0: michael@0: yield this._getShortcutName(installDir); michael@0: michael@0: let backupDir = yield this._backupInstallation(installDir); michael@0: michael@0: try { michael@0: yield this._applyTempInstallation(updateDir, installDir); michael@0: } catch (ex) { michael@0: yield this._restoreInstallation(backupDir, installDir); michael@0: throw ex; michael@0: } finally { michael@0: yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: } michael@0: }), michael@0: michael@0: _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { michael@0: yield moveDirectory(aTmpDir, aInstallDir); michael@0: michael@0: this._createShortcutFiles(aInstallDir); michael@0: this._writeSystemKeys(aInstallDir); michael@0: }), michael@0: michael@0: _getShortcutName: Task.async(function*(aInstallDir) { michael@0: let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI); michael@0: michael@0: if (shortcutLogsINIfile.exists()) { michael@0: // If it's a reinstallation (or an update) get the shortcut names michael@0: // from the shortcut_log.ini file michael@0: let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. michael@0: getService(Ci.nsIINIParserFactory). michael@0: createINIParser(shortcutLogsINIfile); michael@0: this.shortcutName = parser.getString("STARTMENU", "Shortcut0"); michael@0: } else { michael@0: // Check in both directories to see if a shortcut with the same name michael@0: // already exists. michael@0: this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ], michael@0: this.appNameAsFilename, michael@0: ".lnk"); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Remove the current installation michael@0: */ michael@0: _removeInstallation: function(keepProfile, aInstallDir) { michael@0: let uninstallKey; michael@0: try { michael@0: uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, michael@0: "SOFTWARE\\Microsoft\\Windows\\" + michael@0: "CurrentVersion\\Uninstall", michael@0: uninstallKey.ACCESS_WRITE); michael@0: if (uninstallKey.hasChild(this.uninstallSubkeyStr)) { michael@0: uninstallKey.removeChild(this.uninstallSubkeyStr); michael@0: } michael@0: } catch (e) { michael@0: } finally { michael@0: if (uninstallKey) { michael@0: uninstallKey.close(); michael@0: } michael@0: } michael@0: michael@0: let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName), michael@0: OS.Path.join(PROGS_DIR, this.shortcutName) ]; michael@0: michael@0: if (keepProfile) { michael@0: for (let filePath of this.backupFiles) { michael@0: filesToRemove.push(OS.Path.join(aInstallDir, filePath)); michael@0: } michael@0: michael@0: filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); michael@0: } else { michael@0: filesToRemove.push(aInstallDir); michael@0: } michael@0: michael@0: removeFiles(filesToRemove); michael@0: }, michael@0: michael@0: _backupInstallation: Task.async(function*(aInstallDir) { michael@0: let backupDir = OS.Path.join(aInstallDir, "backup"); michael@0: yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); michael@0: yield OS.File.makeDir(backupDir); michael@0: michael@0: for (let filePath of this.backupFiles) { michael@0: yield OS.File.move(OS.Path.join(aInstallDir, filePath), michael@0: OS.Path.join(backupDir, filePath)); michael@0: } michael@0: michael@0: return backupDir; michael@0: }), michael@0: michael@0: _restoreInstallation: function(aBackupDir, aInstallDir) { michael@0: return moveDirectory(aBackupDir, aInstallDir); michael@0: }, michael@0: michael@0: /** michael@0: * Creates the main directory structure. michael@0: */ michael@0: _createDirectoryStructure: Task.async(function*(aDir) { michael@0: yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir)); michael@0: michael@0: yield OS.File.makeDir(OS.Path.join(aDir, OS.Path.dirname(this.iconPath)), michael@0: { from: aDir }); michael@0: }), michael@0: michael@0: /** michael@0: * Copy the webrt executable into the installation directory. michael@0: */ michael@0: _copyWebapprt: function(aDir) { michael@0: return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"), michael@0: OS.Path.join(aDir, this.webapprt)); michael@0: }, michael@0: michael@0: /** michael@0: * Copy the uninstaller executable into the installation directory. michael@0: */ michael@0: _copyUninstaller: function(aDir) { michael@0: return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"), michael@0: OS.Path.join(aDir, this.uninstallerFile)); michael@0: }, michael@0: michael@0: /** michael@0: * Creates the configuration files into their destination folders. michael@0: */ michael@0: _createConfigFiles: function(aDir) { michael@0: // ${InstallDir}/webapp.json michael@0: yield writeToFile(OS.Path.join(aDir, this.configJson), michael@0: JSON.stringify(this.webappJson)); michael@0: michael@0: let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. michael@0: getService(Ci.nsIINIParserFactory); michael@0: michael@0: // ${InstallDir}/webapp.ini michael@0: let webappINIfile = getFile(aDir, this.webappINI); michael@0: michael@0: let writer = factory.createINIParser(webappINIfile) michael@0: .QueryInterface(Ci.nsIINIParserWriter); michael@0: writer.setString("Webapp", "Name", this.appName); michael@0: writer.setString("Webapp", "Profile", this.uniqueName); michael@0: writer.setString("Webapp", "Executable", this.appNameAsFilename); michael@0: writer.setString("WebappRT", "InstallDir", this.runtimeFolder); michael@0: writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); michael@0: michael@0: let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI); michael@0: michael@0: writer = factory.createINIParser(shortcutLogsINIfile) michael@0: .QueryInterface(Ci.nsIINIParserWriter); michael@0: writer.setString("STARTMENU", "Shortcut0", this.shortcutName); michael@0: writer.setString("DESKTOP", "Shortcut0", this.shortcutName); michael@0: writer.setString("TASKBAR", "Migrated", "true"); michael@0: writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); michael@0: michael@0: // ${UninstallDir}/uninstall.log michael@0: let uninstallContent = michael@0: "File: \\webapp.ini\r\n" + michael@0: "File: \\webapp.json\r\n" + michael@0: "File: \\webapprt.old\r\n" + michael@0: "File: \\chrome\\icons\\default\\default.ico"; michael@0: if (this.isPackaged) { michael@0: uninstallContent += "\r\nFile: \\application.zip"; michael@0: } michael@0: michael@0: yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"), michael@0: uninstallContent); michael@0: }, michael@0: michael@0: /** michael@0: * Writes the keys to the system registry that are necessary for the app michael@0: * operation and uninstall process. michael@0: */ michael@0: _writeSystemKeys: function(aInstallDir) { michael@0: let parentKey; michael@0: let uninstallKey; michael@0: let subKey; michael@0: michael@0: try { michael@0: parentKey = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: parentKey.open(parentKey.ROOT_KEY_CURRENT_USER, michael@0: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", michael@0: parentKey.ACCESS_WRITE); michael@0: uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE) michael@0: subKey = uninstallKey.createChild(this.uninstallSubkeyStr, michael@0: uninstallKey.ACCESS_WRITE); michael@0: michael@0: subKey.writeStringValue("DisplayName", this.appName); michael@0: michael@0: let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile); michael@0: michael@0: subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"'); michael@0: subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"'); michael@0: subKey.writeStringValue("AppFilename", this.appNameAsFilename); michael@0: subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir, michael@0: this.iconPath)); michael@0: michael@0: let date = new Date(); michael@0: let year = date.getYear().toString(); michael@0: let month = date.getMonth(); michael@0: if (month < 10) { michael@0: month = "0" + month; michael@0: } michael@0: let day = date.getDate(); michael@0: if (day < 10) { michael@0: day = "0" + day; michael@0: } michael@0: subKey.writeStringValue("InstallDate", year + month + day); michael@0: if (this.version) { michael@0: subKey.writeStringValue("DisplayVersion", this.version); michael@0: } michael@0: if (this.developerName) { michael@0: subKey.writeStringValue("Publisher", this.developerName); michael@0: } michael@0: subKey.writeStringValue("URLInfoAbout", this.developerUrl); michael@0: if (this.size) { michael@0: subKey.writeIntValue("EstimatedSize", this.size); michael@0: } michael@0: michael@0: subKey.writeIntValue("NoModify", 1); michael@0: subKey.writeIntValue("NoRepair", 1); michael@0: } catch(ex) { michael@0: throw ex; michael@0: } finally { michael@0: if(subKey) subKey.close(); michael@0: if(uninstallKey) uninstallKey.close(); michael@0: if(parentKey) parentKey.close(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates a shortcut file inside the app installation folder and makes michael@0: * two copies of it: one into the desktop and one into the start menu. michael@0: */ michael@0: _createShortcutFiles: function(aInstallDir) { michael@0: let shortcut = getFile(aInstallDir, this.shortcutName). michael@0: QueryInterface(Ci.nsILocalFileWin); michael@0: michael@0: /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args, michael@0: description, iconFile, iconIndex) */ michael@0: michael@0: shortcut.setShortcut(getFile(aInstallDir, this.webapprt), michael@0: getFile(aInstallDir), michael@0: null, michael@0: this.shortDescription, michael@0: getFile(aInstallDir, this.iconPath), michael@0: 0); michael@0: michael@0: shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName); michael@0: shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName); michael@0: michael@0: shortcut.followLinks = false; michael@0: shortcut.remove(false); michael@0: }, michael@0: michael@0: /** michael@0: * Process the icon from the imageStream as retrieved from michael@0: * the URL by getIconForApp(). This will save the icon to the michael@0: * topwindow.ico file. michael@0: * michael@0: * @param aMimeType the icon mimetype michael@0: * @param aImageStream the stream for the image data michael@0: * @param aDir the directory where the icon should be stored michael@0: */ michael@0: _processIcon: function(aMimeType, aImageStream, aDir) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let imgTools = Cc["@mozilla.org/image/tools;1"]. michael@0: createInstance(Ci.imgITools); michael@0: michael@0: let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); michael@0: let iconStream = imgTools.encodeImage(imgContainer, michael@0: "image/vnd.microsoft.icon", michael@0: "format=bmp;bpp=32"); michael@0: michael@0: let tmpIconFile = getFile(aDir, this.iconPath); michael@0: michael@0: let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile); michael@0: NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { michael@0: if (Components.isSuccessCode(aResult)) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject("Failure copying icon: " + aResult); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: }