toolkit/webapps/WinNativeApp.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir;
michael@0 6 const APP_DATA_DIR = OS.Constants.Path.winAppDataDir;
michael@0 7
michael@0 8 /*************************************
michael@0 9 * Windows app installer
michael@0 10 *
michael@0 11 * The Windows installation process will generate the following files:
michael@0 12 *
michael@0 13 * ${FolderName} = sanitized app name + "-" + manifest url hash
michael@0 14 *
michael@0 15 * %APPDATA%/${FolderName}
michael@0 16 * - webapp.ini
michael@0 17 * - webapp.json
michael@0 18 * - ${AppName}.exe
michael@0 19 * - ${AppName}.lnk
michael@0 20 * / uninstall
michael@0 21 * - webapp-uninstaller.exe
michael@0 22 * - shortcuts_log.ini
michael@0 23 * - uninstall.log
michael@0 24 * / chrome/icons/default/
michael@0 25 * - default.ico
michael@0 26 *
michael@0 27 * After the app runs for the first time, a profiles/ folder will also be
michael@0 28 * created which will host the user profile for this app.
michael@0 29 */
michael@0 30
michael@0 31 /**
michael@0 32 * Constructor for the Windows native app shell
michael@0 33 *
michael@0 34 * @param aApp {Object} the app object provided to the install function
michael@0 35 * @param aManifest {Object} the manifest data provided by the web app
michael@0 36 * @param aCategories {Array} array of app categories
michael@0 37 * @param aRegistryDir {String} (optional) path to the registry
michael@0 38 */
michael@0 39 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
michael@0 40 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
michael@0 41
michael@0 42 if (this.isPackaged) {
michael@0 43 this.size = aApp.updateManifest.size / 1024;
michael@0 44 }
michael@0 45
michael@0 46 this.webapprt = this.appNameAsFilename + ".exe";
michael@0 47 this.configJson = "webapp.json";
michael@0 48 this.webappINI = "webapp.ini";
michael@0 49 this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico");
michael@0 50 this.uninstallDir = "uninstall";
michael@0 51 this.uninstallerFile = OS.Path.join(this.uninstallDir,
michael@0 52 "webapp-uninstaller.exe");
michael@0 53 this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini");
michael@0 54 this.zipFile = "application.zip";
michael@0 55
michael@0 56 this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ];
michael@0 57 if (this.isPackaged) {
michael@0 58 this.backupFiles.push(this.zipFile);
michael@0 59 }
michael@0 60
michael@0 61 this.uninstallSubkeyStr = this.uniqueName;
michael@0 62 }
michael@0 63
michael@0 64 NativeApp.prototype = {
michael@0 65 __proto__: CommonNativeApp.prototype,
michael@0 66 size: null,
michael@0 67
michael@0 68 /**
michael@0 69 * Creates a native installation of the web app in the OS
michael@0 70 *
michael@0 71 * @param aManifest {Object} the manifest data provided by the web app
michael@0 72 * @param aZipPath {String} path to the zip file for packaged apps (undefined
michael@0 73 * for hosted apps)
michael@0 74 */
michael@0 75 install: Task.async(function*(aManifest, aZipPath) {
michael@0 76 if (this._dryRun) {
michael@0 77 return;
michael@0 78 }
michael@0 79
michael@0 80 // If the application is already installed, this is a reinstallation.
michael@0 81 if (WebappOSUtils.getInstallPath(this.app)) {
michael@0 82 return yield this.prepareUpdate(aManifest, aZipPath);
michael@0 83 }
michael@0 84
michael@0 85 this._setData(aManifest);
michael@0 86
michael@0 87 let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName);
michael@0 88
michael@0 89 // Create a temporary installation directory.
michael@0 90 let dir = getFile(TMP_DIR, this.uniqueName);
michael@0 91 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
michael@0 92 let tmpDir = dir.path;
michael@0 93
michael@0 94 // Perform the installation in the temp directory.
michael@0 95 try {
michael@0 96 yield this._createDirectoryStructure(tmpDir);
michael@0 97 yield this._getShortcutName(installDir);
michael@0 98 yield this._copyWebapprt(tmpDir);
michael@0 99 yield this._copyUninstaller(tmpDir);
michael@0 100 yield this._createConfigFiles(tmpDir);
michael@0 101
michael@0 102 if (aZipPath) {
michael@0 103 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
michael@0 104 }
michael@0 105
michael@0 106 yield this._getIcon(tmpDir);
michael@0 107 } catch (ex) {
michael@0 108 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
michael@0 109 throw ex;
michael@0 110 }
michael@0 111
michael@0 112 // Apply the installation.
michael@0 113 this._removeInstallation(true, installDir);
michael@0 114
michael@0 115 try {
michael@0 116 yield this._applyTempInstallation(tmpDir, installDir);
michael@0 117 } catch (ex) {
michael@0 118 this._removeInstallation(false, installDir);
michael@0 119 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
michael@0 120 throw ex;
michael@0 121 }
michael@0 122 }),
michael@0 123
michael@0 124 /**
michael@0 125 * Creates an update in a temporary directory to be applied later.
michael@0 126 *
michael@0 127 * @param aManifest {Object} the manifest data provided by the web app
michael@0 128 * @param aZipPath {String} path to the zip file for packaged apps (undefined
michael@0 129 * for hosted apps)
michael@0 130 */
michael@0 131 prepareUpdate: Task.async(function*(aManifest, aZipPath) {
michael@0 132 if (this._dryRun) {
michael@0 133 return;
michael@0 134 }
michael@0 135
michael@0 136 this._setData(aManifest);
michael@0 137
michael@0 138 let installDir = WebappOSUtils.getInstallPath(this.app);
michael@0 139 if (!installDir) {
michael@0 140 throw ERR_NOT_INSTALLED;
michael@0 141 }
michael@0 142
michael@0 143 if (this.uniqueName != OS.Path.basename(installDir)) {
michael@0 144 // Bug 919799: If the app is still in the registry, migrate its data to
michael@0 145 // the new format.
michael@0 146 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
michael@0 147 }
michael@0 148
michael@0 149 let updateDir = OS.Path.join(installDir, "update");
michael@0 150 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
michael@0 151 yield OS.File.makeDir(updateDir);
michael@0 152
michael@0 153 // Perform the update in the "update" subdirectory.
michael@0 154 try {
michael@0 155 yield this._createDirectoryStructure(updateDir);
michael@0 156 yield this._getShortcutName(installDir);
michael@0 157 yield this._copyUninstaller(updateDir);
michael@0 158 yield this._createConfigFiles(updateDir);
michael@0 159
michael@0 160 if (aZipPath) {
michael@0 161 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
michael@0 162 }
michael@0 163
michael@0 164 yield this._getIcon(updateDir);
michael@0 165 } catch (ex) {
michael@0 166 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
michael@0 167 throw ex;
michael@0 168 }
michael@0 169 }),
michael@0 170
michael@0 171 /**
michael@0 172 * Applies an update.
michael@0 173 */
michael@0 174 applyUpdate: Task.async(function*() {
michael@0 175 if (this._dryRun) {
michael@0 176 return;
michael@0 177 }
michael@0 178
michael@0 179 let installDir = WebappOSUtils.getInstallPath(this.app);
michael@0 180 let updateDir = OS.Path.join(installDir, "update");
michael@0 181
michael@0 182 yield this._getShortcutName(installDir);
michael@0 183
michael@0 184 let backupDir = yield this._backupInstallation(installDir);
michael@0 185
michael@0 186 try {
michael@0 187 yield this._applyTempInstallation(updateDir, installDir);
michael@0 188 } catch (ex) {
michael@0 189 yield this._restoreInstallation(backupDir, installDir);
michael@0 190 throw ex;
michael@0 191 } finally {
michael@0 192 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
michael@0 193 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
michael@0 194 }
michael@0 195 }),
michael@0 196
michael@0 197 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
michael@0 198 yield moveDirectory(aTmpDir, aInstallDir);
michael@0 199
michael@0 200 this._createShortcutFiles(aInstallDir);
michael@0 201 this._writeSystemKeys(aInstallDir);
michael@0 202 }),
michael@0 203
michael@0 204 _getShortcutName: Task.async(function*(aInstallDir) {
michael@0 205 let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI);
michael@0 206
michael@0 207 if (shortcutLogsINIfile.exists()) {
michael@0 208 // If it's a reinstallation (or an update) get the shortcut names
michael@0 209 // from the shortcut_log.ini file
michael@0 210 let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
michael@0 211 getService(Ci.nsIINIParserFactory).
michael@0 212 createINIParser(shortcutLogsINIfile);
michael@0 213 this.shortcutName = parser.getString("STARTMENU", "Shortcut0");
michael@0 214 } else {
michael@0 215 // Check in both directories to see if a shortcut with the same name
michael@0 216 // already exists.
michael@0 217 this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
michael@0 218 this.appNameAsFilename,
michael@0 219 ".lnk");
michael@0 220 }
michael@0 221 }),
michael@0 222
michael@0 223 /**
michael@0 224 * Remove the current installation
michael@0 225 */
michael@0 226 _removeInstallation: function(keepProfile, aInstallDir) {
michael@0 227 let uninstallKey;
michael@0 228 try {
michael@0 229 uninstallKey = Cc["@mozilla.org/windows-registry-key;1"].
michael@0 230 createInstance(Ci.nsIWindowsRegKey);
michael@0 231 uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
michael@0 232 "SOFTWARE\\Microsoft\\Windows\\" +
michael@0 233 "CurrentVersion\\Uninstall",
michael@0 234 uninstallKey.ACCESS_WRITE);
michael@0 235 if (uninstallKey.hasChild(this.uninstallSubkeyStr)) {
michael@0 236 uninstallKey.removeChild(this.uninstallSubkeyStr);
michael@0 237 }
michael@0 238 } catch (e) {
michael@0 239 } finally {
michael@0 240 if (uninstallKey) {
michael@0 241 uninstallKey.close();
michael@0 242 }
michael@0 243 }
michael@0 244
michael@0 245 let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName),
michael@0 246 OS.Path.join(PROGS_DIR, this.shortcutName) ];
michael@0 247
michael@0 248 if (keepProfile) {
michael@0 249 for (let filePath of this.backupFiles) {
michael@0 250 filesToRemove.push(OS.Path.join(aInstallDir, filePath));
michael@0 251 }
michael@0 252
michael@0 253 filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
michael@0 254 } else {
michael@0 255 filesToRemove.push(aInstallDir);
michael@0 256 }
michael@0 257
michael@0 258 removeFiles(filesToRemove);
michael@0 259 },
michael@0 260
michael@0 261 _backupInstallation: Task.async(function*(aInstallDir) {
michael@0 262 let backupDir = OS.Path.join(aInstallDir, "backup");
michael@0 263 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
michael@0 264 yield OS.File.makeDir(backupDir);
michael@0 265
michael@0 266 for (let filePath of this.backupFiles) {
michael@0 267 yield OS.File.move(OS.Path.join(aInstallDir, filePath),
michael@0 268 OS.Path.join(backupDir, filePath));
michael@0 269 }
michael@0 270
michael@0 271 return backupDir;
michael@0 272 }),
michael@0 273
michael@0 274 _restoreInstallation: function(aBackupDir, aInstallDir) {
michael@0 275 return moveDirectory(aBackupDir, aInstallDir);
michael@0 276 },
michael@0 277
michael@0 278 /**
michael@0 279 * Creates the main directory structure.
michael@0 280 */
michael@0 281 _createDirectoryStructure: Task.async(function*(aDir) {
michael@0 282 yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir));
michael@0 283
michael@0 284 yield OS.File.makeDir(OS.Path.join(aDir, OS.Path.dirname(this.iconPath)),
michael@0 285 { from: aDir });
michael@0 286 }),
michael@0 287
michael@0 288 /**
michael@0 289 * Copy the webrt executable into the installation directory.
michael@0 290 */
michael@0 291 _copyWebapprt: function(aDir) {
michael@0 292 return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"),
michael@0 293 OS.Path.join(aDir, this.webapprt));
michael@0 294 },
michael@0 295
michael@0 296 /**
michael@0 297 * Copy the uninstaller executable into the installation directory.
michael@0 298 */
michael@0 299 _copyUninstaller: function(aDir) {
michael@0 300 return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"),
michael@0 301 OS.Path.join(aDir, this.uninstallerFile));
michael@0 302 },
michael@0 303
michael@0 304 /**
michael@0 305 * Creates the configuration files into their destination folders.
michael@0 306 */
michael@0 307 _createConfigFiles: function(aDir) {
michael@0 308 // ${InstallDir}/webapp.json
michael@0 309 yield writeToFile(OS.Path.join(aDir, this.configJson),
michael@0 310 JSON.stringify(this.webappJson));
michael@0 311
michael@0 312 let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
michael@0 313 getService(Ci.nsIINIParserFactory);
michael@0 314
michael@0 315 // ${InstallDir}/webapp.ini
michael@0 316 let webappINIfile = getFile(aDir, this.webappINI);
michael@0 317
michael@0 318 let writer = factory.createINIParser(webappINIfile)
michael@0 319 .QueryInterface(Ci.nsIINIParserWriter);
michael@0 320 writer.setString("Webapp", "Name", this.appName);
michael@0 321 writer.setString("Webapp", "Profile", this.uniqueName);
michael@0 322 writer.setString("Webapp", "Executable", this.appNameAsFilename);
michael@0 323 writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
michael@0 324 writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
michael@0 325
michael@0 326 let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI);
michael@0 327
michael@0 328 writer = factory.createINIParser(shortcutLogsINIfile)
michael@0 329 .QueryInterface(Ci.nsIINIParserWriter);
michael@0 330 writer.setString("STARTMENU", "Shortcut0", this.shortcutName);
michael@0 331 writer.setString("DESKTOP", "Shortcut0", this.shortcutName);
michael@0 332 writer.setString("TASKBAR", "Migrated", "true");
michael@0 333 writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
michael@0 334
michael@0 335 // ${UninstallDir}/uninstall.log
michael@0 336 let uninstallContent =
michael@0 337 "File: \\webapp.ini\r\n" +
michael@0 338 "File: \\webapp.json\r\n" +
michael@0 339 "File: \\webapprt.old\r\n" +
michael@0 340 "File: \\chrome\\icons\\default\\default.ico";
michael@0 341 if (this.isPackaged) {
michael@0 342 uninstallContent += "\r\nFile: \\application.zip";
michael@0 343 }
michael@0 344
michael@0 345 yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"),
michael@0 346 uninstallContent);
michael@0 347 },
michael@0 348
michael@0 349 /**
michael@0 350 * Writes the keys to the system registry that are necessary for the app
michael@0 351 * operation and uninstall process.
michael@0 352 */
michael@0 353 _writeSystemKeys: function(aInstallDir) {
michael@0 354 let parentKey;
michael@0 355 let uninstallKey;
michael@0 356 let subKey;
michael@0 357
michael@0 358 try {
michael@0 359 parentKey = Cc["@mozilla.org/windows-registry-key;1"].
michael@0 360 createInstance(Ci.nsIWindowsRegKey);
michael@0 361 parentKey.open(parentKey.ROOT_KEY_CURRENT_USER,
michael@0 362 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
michael@0 363 parentKey.ACCESS_WRITE);
michael@0 364 uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE)
michael@0 365 subKey = uninstallKey.createChild(this.uninstallSubkeyStr,
michael@0 366 uninstallKey.ACCESS_WRITE);
michael@0 367
michael@0 368 subKey.writeStringValue("DisplayName", this.appName);
michael@0 369
michael@0 370 let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile);
michael@0 371
michael@0 372 subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"');
michael@0 373 subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"');
michael@0 374 subKey.writeStringValue("AppFilename", this.appNameAsFilename);
michael@0 375 subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir,
michael@0 376 this.iconPath));
michael@0 377
michael@0 378 let date = new Date();
michael@0 379 let year = date.getYear().toString();
michael@0 380 let month = date.getMonth();
michael@0 381 if (month < 10) {
michael@0 382 month = "0" + month;
michael@0 383 }
michael@0 384 let day = date.getDate();
michael@0 385 if (day < 10) {
michael@0 386 day = "0" + day;
michael@0 387 }
michael@0 388 subKey.writeStringValue("InstallDate", year + month + day);
michael@0 389 if (this.version) {
michael@0 390 subKey.writeStringValue("DisplayVersion", this.version);
michael@0 391 }
michael@0 392 if (this.developerName) {
michael@0 393 subKey.writeStringValue("Publisher", this.developerName);
michael@0 394 }
michael@0 395 subKey.writeStringValue("URLInfoAbout", this.developerUrl);
michael@0 396 if (this.size) {
michael@0 397 subKey.writeIntValue("EstimatedSize", this.size);
michael@0 398 }
michael@0 399
michael@0 400 subKey.writeIntValue("NoModify", 1);
michael@0 401 subKey.writeIntValue("NoRepair", 1);
michael@0 402 } catch(ex) {
michael@0 403 throw ex;
michael@0 404 } finally {
michael@0 405 if(subKey) subKey.close();
michael@0 406 if(uninstallKey) uninstallKey.close();
michael@0 407 if(parentKey) parentKey.close();
michael@0 408 }
michael@0 409 },
michael@0 410
michael@0 411 /**
michael@0 412 * Creates a shortcut file inside the app installation folder and makes
michael@0 413 * two copies of it: one into the desktop and one into the start menu.
michael@0 414 */
michael@0 415 _createShortcutFiles: function(aInstallDir) {
michael@0 416 let shortcut = getFile(aInstallDir, this.shortcutName).
michael@0 417 QueryInterface(Ci.nsILocalFileWin);
michael@0 418
michael@0 419 /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args,
michael@0 420 description, iconFile, iconIndex) */
michael@0 421
michael@0 422 shortcut.setShortcut(getFile(aInstallDir, this.webapprt),
michael@0 423 getFile(aInstallDir),
michael@0 424 null,
michael@0 425 this.shortDescription,
michael@0 426 getFile(aInstallDir, this.iconPath),
michael@0 427 0);
michael@0 428
michael@0 429 shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName);
michael@0 430 shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName);
michael@0 431
michael@0 432 shortcut.followLinks = false;
michael@0 433 shortcut.remove(false);
michael@0 434 },
michael@0 435
michael@0 436 /**
michael@0 437 * Process the icon from the imageStream as retrieved from
michael@0 438 * the URL by getIconForApp(). This will save the icon to the
michael@0 439 * topwindow.ico file.
michael@0 440 *
michael@0 441 * @param aMimeType the icon mimetype
michael@0 442 * @param aImageStream the stream for the image data
michael@0 443 * @param aDir the directory where the icon should be stored
michael@0 444 */
michael@0 445 _processIcon: function(aMimeType, aImageStream, aDir) {
michael@0 446 let deferred = Promise.defer();
michael@0 447
michael@0 448 let imgTools = Cc["@mozilla.org/image/tools;1"].
michael@0 449 createInstance(Ci.imgITools);
michael@0 450
michael@0 451 let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
michael@0 452 let iconStream = imgTools.encodeImage(imgContainer,
michael@0 453 "image/vnd.microsoft.icon",
michael@0 454 "format=bmp;bpp=32");
michael@0 455
michael@0 456 let tmpIconFile = getFile(aDir, this.iconPath);
michael@0 457
michael@0 458 let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile);
michael@0 459 NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
michael@0 460 if (Components.isSuccessCode(aResult)) {
michael@0 461 deferred.resolve();
michael@0 462 } else {
michael@0 463 deferred.reject("Failure copying icon: " + aResult);
michael@0 464 }
michael@0 465 });
michael@0 466
michael@0 467 return deferred.promise;
michael@0 468 }
michael@0 469 }

mercurial