1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/webapps/WinNativeApp.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,469 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir; 1.9 +const APP_DATA_DIR = OS.Constants.Path.winAppDataDir; 1.10 + 1.11 +/************************************* 1.12 + * Windows app installer 1.13 + * 1.14 + * The Windows installation process will generate the following files: 1.15 + * 1.16 + * ${FolderName} = sanitized app name + "-" + manifest url hash 1.17 + * 1.18 + * %APPDATA%/${FolderName} 1.19 + * - webapp.ini 1.20 + * - webapp.json 1.21 + * - ${AppName}.exe 1.22 + * - ${AppName}.lnk 1.23 + * / uninstall 1.24 + * - webapp-uninstaller.exe 1.25 + * - shortcuts_log.ini 1.26 + * - uninstall.log 1.27 + * / chrome/icons/default/ 1.28 + * - default.ico 1.29 + * 1.30 + * After the app runs for the first time, a profiles/ folder will also be 1.31 + * created which will host the user profile for this app. 1.32 + */ 1.33 + 1.34 +/** 1.35 + * Constructor for the Windows native app shell 1.36 + * 1.37 + * @param aApp {Object} the app object provided to the install function 1.38 + * @param aManifest {Object} the manifest data provided by the web app 1.39 + * @param aCategories {Array} array of app categories 1.40 + * @param aRegistryDir {String} (optional) path to the registry 1.41 + */ 1.42 +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { 1.43 + CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); 1.44 + 1.45 + if (this.isPackaged) { 1.46 + this.size = aApp.updateManifest.size / 1024; 1.47 + } 1.48 + 1.49 + this.webapprt = this.appNameAsFilename + ".exe"; 1.50 + this.configJson = "webapp.json"; 1.51 + this.webappINI = "webapp.ini"; 1.52 + this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico"); 1.53 + this.uninstallDir = "uninstall"; 1.54 + this.uninstallerFile = OS.Path.join(this.uninstallDir, 1.55 + "webapp-uninstaller.exe"); 1.56 + this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini"); 1.57 + this.zipFile = "application.zip"; 1.58 + 1.59 + this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ]; 1.60 + if (this.isPackaged) { 1.61 + this.backupFiles.push(this.zipFile); 1.62 + } 1.63 + 1.64 + this.uninstallSubkeyStr = this.uniqueName; 1.65 +} 1.66 + 1.67 +NativeApp.prototype = { 1.68 + __proto__: CommonNativeApp.prototype, 1.69 + size: null, 1.70 + 1.71 + /** 1.72 + * Creates a native installation of the web app in the OS 1.73 + * 1.74 + * @param aManifest {Object} the manifest data provided by the web app 1.75 + * @param aZipPath {String} path to the zip file for packaged apps (undefined 1.76 + * for hosted apps) 1.77 + */ 1.78 + install: Task.async(function*(aManifest, aZipPath) { 1.79 + if (this._dryRun) { 1.80 + return; 1.81 + } 1.82 + 1.83 + // If the application is already installed, this is a reinstallation. 1.84 + if (WebappOSUtils.getInstallPath(this.app)) { 1.85 + return yield this.prepareUpdate(aManifest, aZipPath); 1.86 + } 1.87 + 1.88 + this._setData(aManifest); 1.89 + 1.90 + let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName); 1.91 + 1.92 + // Create a temporary installation directory. 1.93 + let dir = getFile(TMP_DIR, this.uniqueName); 1.94 + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); 1.95 + let tmpDir = dir.path; 1.96 + 1.97 + // Perform the installation in the temp directory. 1.98 + try { 1.99 + yield this._createDirectoryStructure(tmpDir); 1.100 + yield this._getShortcutName(installDir); 1.101 + yield this._copyWebapprt(tmpDir); 1.102 + yield this._copyUninstaller(tmpDir); 1.103 + yield this._createConfigFiles(tmpDir); 1.104 + 1.105 + if (aZipPath) { 1.106 + yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); 1.107 + } 1.108 + 1.109 + yield this._getIcon(tmpDir); 1.110 + } catch (ex) { 1.111 + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); 1.112 + throw ex; 1.113 + } 1.114 + 1.115 + // Apply the installation. 1.116 + this._removeInstallation(true, installDir); 1.117 + 1.118 + try { 1.119 + yield this._applyTempInstallation(tmpDir, installDir); 1.120 + } catch (ex) { 1.121 + this._removeInstallation(false, installDir); 1.122 + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); 1.123 + throw ex; 1.124 + } 1.125 + }), 1.126 + 1.127 + /** 1.128 + * Creates an update in a temporary directory to be applied later. 1.129 + * 1.130 + * @param aManifest {Object} the manifest data provided by the web app 1.131 + * @param aZipPath {String} path to the zip file for packaged apps (undefined 1.132 + * for hosted apps) 1.133 + */ 1.134 + prepareUpdate: Task.async(function*(aManifest, aZipPath) { 1.135 + if (this._dryRun) { 1.136 + return; 1.137 + } 1.138 + 1.139 + this._setData(aManifest); 1.140 + 1.141 + let installDir = WebappOSUtils.getInstallPath(this.app); 1.142 + if (!installDir) { 1.143 + throw ERR_NOT_INSTALLED; 1.144 + } 1.145 + 1.146 + if (this.uniqueName != OS.Path.basename(installDir)) { 1.147 + // Bug 919799: If the app is still in the registry, migrate its data to 1.148 + // the new format. 1.149 + throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; 1.150 + } 1.151 + 1.152 + let updateDir = OS.Path.join(installDir, "update"); 1.153 + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); 1.154 + yield OS.File.makeDir(updateDir); 1.155 + 1.156 + // Perform the update in the "update" subdirectory. 1.157 + try { 1.158 + yield this._createDirectoryStructure(updateDir); 1.159 + yield this._getShortcutName(installDir); 1.160 + yield this._copyUninstaller(updateDir); 1.161 + yield this._createConfigFiles(updateDir); 1.162 + 1.163 + if (aZipPath) { 1.164 + yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); 1.165 + } 1.166 + 1.167 + yield this._getIcon(updateDir); 1.168 + } catch (ex) { 1.169 + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); 1.170 + throw ex; 1.171 + } 1.172 + }), 1.173 + 1.174 + /** 1.175 + * Applies an update. 1.176 + */ 1.177 + applyUpdate: Task.async(function*() { 1.178 + if (this._dryRun) { 1.179 + return; 1.180 + } 1.181 + 1.182 + let installDir = WebappOSUtils.getInstallPath(this.app); 1.183 + let updateDir = OS.Path.join(installDir, "update"); 1.184 + 1.185 + yield this._getShortcutName(installDir); 1.186 + 1.187 + let backupDir = yield this._backupInstallation(installDir); 1.188 + 1.189 + try { 1.190 + yield this._applyTempInstallation(updateDir, installDir); 1.191 + } catch (ex) { 1.192 + yield this._restoreInstallation(backupDir, installDir); 1.193 + throw ex; 1.194 + } finally { 1.195 + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); 1.196 + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); 1.197 + } 1.198 + }), 1.199 + 1.200 + _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { 1.201 + yield moveDirectory(aTmpDir, aInstallDir); 1.202 + 1.203 + this._createShortcutFiles(aInstallDir); 1.204 + this._writeSystemKeys(aInstallDir); 1.205 + }), 1.206 + 1.207 + _getShortcutName: Task.async(function*(aInstallDir) { 1.208 + let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI); 1.209 + 1.210 + if (shortcutLogsINIfile.exists()) { 1.211 + // If it's a reinstallation (or an update) get the shortcut names 1.212 + // from the shortcut_log.ini file 1.213 + let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. 1.214 + getService(Ci.nsIINIParserFactory). 1.215 + createINIParser(shortcutLogsINIfile); 1.216 + this.shortcutName = parser.getString("STARTMENU", "Shortcut0"); 1.217 + } else { 1.218 + // Check in both directories to see if a shortcut with the same name 1.219 + // already exists. 1.220 + this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ], 1.221 + this.appNameAsFilename, 1.222 + ".lnk"); 1.223 + } 1.224 + }), 1.225 + 1.226 + /** 1.227 + * Remove the current installation 1.228 + */ 1.229 + _removeInstallation: function(keepProfile, aInstallDir) { 1.230 + let uninstallKey; 1.231 + try { 1.232 + uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]. 1.233 + createInstance(Ci.nsIWindowsRegKey); 1.234 + uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, 1.235 + "SOFTWARE\\Microsoft\\Windows\\" + 1.236 + "CurrentVersion\\Uninstall", 1.237 + uninstallKey.ACCESS_WRITE); 1.238 + if (uninstallKey.hasChild(this.uninstallSubkeyStr)) { 1.239 + uninstallKey.removeChild(this.uninstallSubkeyStr); 1.240 + } 1.241 + } catch (e) { 1.242 + } finally { 1.243 + if (uninstallKey) { 1.244 + uninstallKey.close(); 1.245 + } 1.246 + } 1.247 + 1.248 + let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName), 1.249 + OS.Path.join(PROGS_DIR, this.shortcutName) ]; 1.250 + 1.251 + if (keepProfile) { 1.252 + for (let filePath of this.backupFiles) { 1.253 + filesToRemove.push(OS.Path.join(aInstallDir, filePath)); 1.254 + } 1.255 + 1.256 + filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); 1.257 + } else { 1.258 + filesToRemove.push(aInstallDir); 1.259 + } 1.260 + 1.261 + removeFiles(filesToRemove); 1.262 + }, 1.263 + 1.264 + _backupInstallation: Task.async(function*(aInstallDir) { 1.265 + let backupDir = OS.Path.join(aInstallDir, "backup"); 1.266 + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); 1.267 + yield OS.File.makeDir(backupDir); 1.268 + 1.269 + for (let filePath of this.backupFiles) { 1.270 + yield OS.File.move(OS.Path.join(aInstallDir, filePath), 1.271 + OS.Path.join(backupDir, filePath)); 1.272 + } 1.273 + 1.274 + return backupDir; 1.275 + }), 1.276 + 1.277 + _restoreInstallation: function(aBackupDir, aInstallDir) { 1.278 + return moveDirectory(aBackupDir, aInstallDir); 1.279 + }, 1.280 + 1.281 + /** 1.282 + * Creates the main directory structure. 1.283 + */ 1.284 + _createDirectoryStructure: Task.async(function*(aDir) { 1.285 + yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir)); 1.286 + 1.287 + yield OS.File.makeDir(OS.Path.join(aDir, OS.Path.dirname(this.iconPath)), 1.288 + { from: aDir }); 1.289 + }), 1.290 + 1.291 + /** 1.292 + * Copy the webrt executable into the installation directory. 1.293 + */ 1.294 + _copyWebapprt: function(aDir) { 1.295 + return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"), 1.296 + OS.Path.join(aDir, this.webapprt)); 1.297 + }, 1.298 + 1.299 + /** 1.300 + * Copy the uninstaller executable into the installation directory. 1.301 + */ 1.302 + _copyUninstaller: function(aDir) { 1.303 + return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"), 1.304 + OS.Path.join(aDir, this.uninstallerFile)); 1.305 + }, 1.306 + 1.307 + /** 1.308 + * Creates the configuration files into their destination folders. 1.309 + */ 1.310 + _createConfigFiles: function(aDir) { 1.311 + // ${InstallDir}/webapp.json 1.312 + yield writeToFile(OS.Path.join(aDir, this.configJson), 1.313 + JSON.stringify(this.webappJson)); 1.314 + 1.315 + let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. 1.316 + getService(Ci.nsIINIParserFactory); 1.317 + 1.318 + // ${InstallDir}/webapp.ini 1.319 + let webappINIfile = getFile(aDir, this.webappINI); 1.320 + 1.321 + let writer = factory.createINIParser(webappINIfile) 1.322 + .QueryInterface(Ci.nsIINIParserWriter); 1.323 + writer.setString("Webapp", "Name", this.appName); 1.324 + writer.setString("Webapp", "Profile", this.uniqueName); 1.325 + writer.setString("Webapp", "Executable", this.appNameAsFilename); 1.326 + writer.setString("WebappRT", "InstallDir", this.runtimeFolder); 1.327 + writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); 1.328 + 1.329 + let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI); 1.330 + 1.331 + writer = factory.createINIParser(shortcutLogsINIfile) 1.332 + .QueryInterface(Ci.nsIINIParserWriter); 1.333 + writer.setString("STARTMENU", "Shortcut0", this.shortcutName); 1.334 + writer.setString("DESKTOP", "Shortcut0", this.shortcutName); 1.335 + writer.setString("TASKBAR", "Migrated", "true"); 1.336 + writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); 1.337 + 1.338 + // ${UninstallDir}/uninstall.log 1.339 + let uninstallContent = 1.340 + "File: \\webapp.ini\r\n" + 1.341 + "File: \\webapp.json\r\n" + 1.342 + "File: \\webapprt.old\r\n" + 1.343 + "File: \\chrome\\icons\\default\\default.ico"; 1.344 + if (this.isPackaged) { 1.345 + uninstallContent += "\r\nFile: \\application.zip"; 1.346 + } 1.347 + 1.348 + yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"), 1.349 + uninstallContent); 1.350 + }, 1.351 + 1.352 + /** 1.353 + * Writes the keys to the system registry that are necessary for the app 1.354 + * operation and uninstall process. 1.355 + */ 1.356 + _writeSystemKeys: function(aInstallDir) { 1.357 + let parentKey; 1.358 + let uninstallKey; 1.359 + let subKey; 1.360 + 1.361 + try { 1.362 + parentKey = Cc["@mozilla.org/windows-registry-key;1"]. 1.363 + createInstance(Ci.nsIWindowsRegKey); 1.364 + parentKey.open(parentKey.ROOT_KEY_CURRENT_USER, 1.365 + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", 1.366 + parentKey.ACCESS_WRITE); 1.367 + uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE) 1.368 + subKey = uninstallKey.createChild(this.uninstallSubkeyStr, 1.369 + uninstallKey.ACCESS_WRITE); 1.370 + 1.371 + subKey.writeStringValue("DisplayName", this.appName); 1.372 + 1.373 + let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile); 1.374 + 1.375 + subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"'); 1.376 + subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"'); 1.377 + subKey.writeStringValue("AppFilename", this.appNameAsFilename); 1.378 + subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir, 1.379 + this.iconPath)); 1.380 + 1.381 + let date = new Date(); 1.382 + let year = date.getYear().toString(); 1.383 + let month = date.getMonth(); 1.384 + if (month < 10) { 1.385 + month = "0" + month; 1.386 + } 1.387 + let day = date.getDate(); 1.388 + if (day < 10) { 1.389 + day = "0" + day; 1.390 + } 1.391 + subKey.writeStringValue("InstallDate", year + month + day); 1.392 + if (this.version) { 1.393 + subKey.writeStringValue("DisplayVersion", this.version); 1.394 + } 1.395 + if (this.developerName) { 1.396 + subKey.writeStringValue("Publisher", this.developerName); 1.397 + } 1.398 + subKey.writeStringValue("URLInfoAbout", this.developerUrl); 1.399 + if (this.size) { 1.400 + subKey.writeIntValue("EstimatedSize", this.size); 1.401 + } 1.402 + 1.403 + subKey.writeIntValue("NoModify", 1); 1.404 + subKey.writeIntValue("NoRepair", 1); 1.405 + } catch(ex) { 1.406 + throw ex; 1.407 + } finally { 1.408 + if(subKey) subKey.close(); 1.409 + if(uninstallKey) uninstallKey.close(); 1.410 + if(parentKey) parentKey.close(); 1.411 + } 1.412 + }, 1.413 + 1.414 + /** 1.415 + * Creates a shortcut file inside the app installation folder and makes 1.416 + * two copies of it: one into the desktop and one into the start menu. 1.417 + */ 1.418 + _createShortcutFiles: function(aInstallDir) { 1.419 + let shortcut = getFile(aInstallDir, this.shortcutName). 1.420 + QueryInterface(Ci.nsILocalFileWin); 1.421 + 1.422 + /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args, 1.423 + description, iconFile, iconIndex) */ 1.424 + 1.425 + shortcut.setShortcut(getFile(aInstallDir, this.webapprt), 1.426 + getFile(aInstallDir), 1.427 + null, 1.428 + this.shortDescription, 1.429 + getFile(aInstallDir, this.iconPath), 1.430 + 0); 1.431 + 1.432 + shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName); 1.433 + shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName); 1.434 + 1.435 + shortcut.followLinks = false; 1.436 + shortcut.remove(false); 1.437 + }, 1.438 + 1.439 + /** 1.440 + * Process the icon from the imageStream as retrieved from 1.441 + * the URL by getIconForApp(). This will save the icon to the 1.442 + * topwindow.ico file. 1.443 + * 1.444 + * @param aMimeType the icon mimetype 1.445 + * @param aImageStream the stream for the image data 1.446 + * @param aDir the directory where the icon should be stored 1.447 + */ 1.448 + _processIcon: function(aMimeType, aImageStream, aDir) { 1.449 + let deferred = Promise.defer(); 1.450 + 1.451 + let imgTools = Cc["@mozilla.org/image/tools;1"]. 1.452 + createInstance(Ci.imgITools); 1.453 + 1.454 + let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); 1.455 + let iconStream = imgTools.encodeImage(imgContainer, 1.456 + "image/vnd.microsoft.icon", 1.457 + "format=bmp;bpp=32"); 1.458 + 1.459 + let tmpIconFile = getFile(aDir, this.iconPath); 1.460 + 1.461 + let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile); 1.462 + NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { 1.463 + if (Components.isSuccessCode(aResult)) { 1.464 + deferred.resolve(); 1.465 + } else { 1.466 + deferred.reject("Failure copying icon: " + aResult); 1.467 + } 1.468 + }); 1.469 + 1.470 + return deferred.promise; 1.471 + } 1.472 +}