toolkit/webapps/WinNativeApp.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial