toolkit/webapps/WinNativeApp.js

changeset 0
6474c204b198
     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 +}

mercurial