toolkit/webapps/LinuxNativeApp.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/webapps/LinuxNativeApp.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,338 @@
     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 +/**
     1.9 + * Constructor for the Linux native app shell
    1.10 + *
    1.11 + * @param aApp {Object} the app object provided to the install function
    1.12 + * @param aManifest {Object} the manifest data provided by the web app
    1.13 + * @param aCategories {Array} array of app categories
    1.14 + * @param aRegistryDir {String} (optional) path to the registry
    1.15 + */
    1.16 +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
    1.17 +  CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
    1.18 +
    1.19 +  this.iconFile = "icon.png";
    1.20 +  this.webapprt = "webapprt-stub";
    1.21 +  this.configJson = "webapp.json";
    1.22 +  this.webappINI = "webapp.ini";
    1.23 +  this.zipFile = "application.zip";
    1.24 +
    1.25 +  this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ];
    1.26 +  if (this.isPackaged) {
    1.27 +    this.backupFiles.push(this.zipFile);
    1.28 +  }
    1.29 +
    1.30 +  let xdg_data_home = Cc["@mozilla.org/process/environment;1"].
    1.31 +                      getService(Ci.nsIEnvironment).
    1.32 +                      get("XDG_DATA_HOME");
    1.33 +  if (!xdg_data_home) {
    1.34 +    xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share");
    1.35 +  }
    1.36 +
    1.37 +  // The desktop file name is: "owa-" + sanitized app name +
    1.38 +  // "-" + manifest url hash.
    1.39 +  this.desktopINI = OS.Path.join(xdg_data_home, "applications",
    1.40 +                                 "owa-" + this.uniqueName + ".desktop");
    1.41 +}
    1.42 +
    1.43 +NativeApp.prototype = {
    1.44 +  __proto__: CommonNativeApp.prototype,
    1.45 +
    1.46 +  /**
    1.47 +   * Creates a native installation of the web app in the OS
    1.48 +   *
    1.49 +   * @param aManifest {Object} the manifest data provided by the web app
    1.50 +   * @param aZipPath {String} path to the zip file for packaged apps (undefined
    1.51 +   *                          for hosted apps)
    1.52 +   */
    1.53 +  install: Task.async(function*(aManifest, aZipPath) {
    1.54 +    if (this._dryRun) {
    1.55 +      return;
    1.56 +    }
    1.57 +
    1.58 +    // If the application is already installed, this is a reinstallation.
    1.59 +    if (WebappOSUtils.getInstallPath(this.app)) {
    1.60 +      return yield this.prepareUpdate(aManifest, aZipPath);
    1.61 +    }
    1.62 +
    1.63 +    this._setData(aManifest);
    1.64 +
    1.65 +    // The installation directory name is: sanitized app name +
    1.66 +    // "-" + manifest url hash.
    1.67 +    let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName);
    1.68 +
    1.69 +    let dir = getFile(TMP_DIR, this.uniqueName);
    1.70 +    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
    1.71 +    let tmpDir = dir.path;
    1.72 +
    1.73 +    // Create the installation in a temporary directory.
    1.74 +    try {
    1.75 +      this._copyPrebuiltFiles(tmpDir);
    1.76 +      yield this._createConfigFiles(tmpDir);
    1.77 +
    1.78 +      if (aZipPath) {
    1.79 +        yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
    1.80 +      }
    1.81 +
    1.82 +      yield this._getIcon(tmpDir);
    1.83 +    } catch (ex) {
    1.84 +      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
    1.85 +      throw ex;
    1.86 +    }
    1.87 +
    1.88 +    // Apply the installation.
    1.89 +    this._removeInstallation(true, installDir);
    1.90 +
    1.91 +    try {
    1.92 +      yield this._applyTempInstallation(tmpDir, installDir);
    1.93 +    } catch (ex) {
    1.94 +      this._removeInstallation(false, installDir);
    1.95 +      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
    1.96 +      throw ex;
    1.97 +    }
    1.98 +  }),
    1.99 +
   1.100 +  /**
   1.101 +   * Creates an update in a temporary directory to be applied later.
   1.102 +   *
   1.103 +   * @param aManifest {Object} the manifest data provided by the web app
   1.104 +   * @param aZipPath {String} path to the zip file for packaged apps (undefined
   1.105 +   *                          for hosted apps)
   1.106 +   */
   1.107 +  prepareUpdate: Task.async(function*(aManifest, aZipPath) {
   1.108 +    if (this._dryRun) {
   1.109 +      return;
   1.110 +    }
   1.111 +
   1.112 +    this._setData(aManifest);
   1.113 +
   1.114 +    let installDir = WebappOSUtils.getInstallPath(this.app);
   1.115 +    if (!installDir) {
   1.116 +      throw ERR_NOT_INSTALLED;
   1.117 +    }
   1.118 +
   1.119 +    let baseName = OS.Path.basename(installDir)
   1.120 +    let oldUniqueName = baseName.substring(1, baseName.length);
   1.121 +    if (this.uniqueName != oldUniqueName) {
   1.122 +      // Bug 919799: If the app is still in the registry, migrate its data to
   1.123 +      // the new format.
   1.124 +      throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
   1.125 +    }
   1.126 +
   1.127 +    let updateDir = OS.Path.join(installDir, "update");
   1.128 +    yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   1.129 +    yield OS.File.makeDir(updateDir);
   1.130 +
   1.131 +    try {
   1.132 +      yield this._createConfigFiles(updateDir);
   1.133 +
   1.134 +      if (aZipPath) {
   1.135 +        yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
   1.136 +      }
   1.137 +
   1.138 +      yield this._getIcon(updateDir);
   1.139 +    } catch (ex) {
   1.140 +      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   1.141 +      throw ex;
   1.142 +    }
   1.143 +  }),
   1.144 +
   1.145 +  /**
   1.146 +   * Applies an update.
   1.147 +   */
   1.148 +  applyUpdate: Task.async(function*() {
   1.149 +    if (this._dryRun) {
   1.150 +      return;
   1.151 +    }
   1.152 +
   1.153 +    let installDir = WebappOSUtils.getInstallPath(this.app);
   1.154 +    let updateDir = OS.Path.join(installDir, "update");
   1.155 +
   1.156 +    let backupDir = yield this._backupInstallation(installDir);
   1.157 +
   1.158 +    try {
   1.159 +      yield this._applyTempInstallation(updateDir, installDir);
   1.160 +    } catch (ex) {
   1.161 +      yield this._restoreInstallation(backupDir, installDir);
   1.162 +      throw ex;
   1.163 +    } finally {
   1.164 +      yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
   1.165 +      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   1.166 +    }
   1.167 +  }),
   1.168 +
   1.169 +  _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
   1.170 +    yield moveDirectory(aTmpDir, aInstallDir);
   1.171 +
   1.172 +    this._createSystemFiles(aInstallDir);
   1.173 +  }),
   1.174 +
   1.175 +  _removeInstallation: function(keepProfile, aInstallDir) {
   1.176 +    let filesToRemove = [this.desktopINI];
   1.177 +
   1.178 +    if (keepProfile) {
   1.179 +      for (let filePath of this.backupFiles) {
   1.180 +        filesToRemove.push(OS.Path.join(aInstallDir, filePath));
   1.181 +      }
   1.182 +
   1.183 +      filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
   1.184 +    } else {
   1.185 +      filesToRemove.push(aInstallDir);
   1.186 +    }
   1.187 +
   1.188 +    removeFiles(filesToRemove);
   1.189 +  },
   1.190 +
   1.191 +  _backupInstallation: Task.async(function*(aInstallDir) {
   1.192 +    let backupDir = OS.Path.join(aInstallDir, "backup");
   1.193 +    yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
   1.194 +    yield OS.File.makeDir(backupDir);
   1.195 +
   1.196 +    for (let filePath of this.backupFiles) {
   1.197 +      yield OS.File.move(OS.Path.join(aInstallDir, filePath),
   1.198 +                         OS.Path.join(backupDir, filePath));
   1.199 +    }
   1.200 +
   1.201 +    return backupDir;
   1.202 +  }),
   1.203 +
   1.204 +  _restoreInstallation: function(aBackupDir, aInstallDir) {
   1.205 +    return moveDirectory(aBackupDir, aInstallDir);
   1.206 +  },
   1.207 +
   1.208 +  _copyPrebuiltFiles: function(aDir) {
   1.209 +    let destDir = getFile(aDir);
   1.210 +    let stub = getFile(this.runtimeFolder, this.webapprt);
   1.211 +    stub.copyTo(destDir, null);
   1.212 +  },
   1.213 +
   1.214 +  /**
   1.215 +   * Translate marketplace categories to freedesktop.org categories.
   1.216 +   *
   1.217 +   * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry
   1.218 +   *
   1.219 +   * @return an array of categories
   1.220 +   */
   1.221 +  _translateCategories: function() {
   1.222 +    let translations = {
   1.223 +      "books": "Education;Literature",
   1.224 +      "business": "Finance",
   1.225 +      "education": "Education",
   1.226 +      "entertainment": "Amusement",
   1.227 +      "sports": "Sports",
   1.228 +      "games": "Game",
   1.229 +      "health-fitness": "MedicalSoftware",
   1.230 +      "lifestyle": "Amusement",
   1.231 +      "music": "Audio;Music",
   1.232 +      "news-weather": "News",
   1.233 +      "photo-video": "Video;AudioVideo;Photography",
   1.234 +      "productivity": "Office",
   1.235 +      "shopping": "Amusement",
   1.236 +      "social": "Chat",
   1.237 +      "travel": "Amusement",
   1.238 +      "reference": "Science;Education;Documentation",
   1.239 +      "maps-navigation": "Maps",
   1.240 +      "utilities": "Utility"
   1.241 +    };
   1.242 +
   1.243 +    // The trailing semicolon is needed as written in the freedesktop specification
   1.244 +    let categories = "";
   1.245 +    for (let category of this.categories) {
   1.246 +      let catLower = category.toLowerCase();
   1.247 +      if (catLower in translations) {
   1.248 +        categories += translations[catLower] + ";";
   1.249 +      }
   1.250 +    }
   1.251 +
   1.252 +    return categories;
   1.253 +  },
   1.254 +
   1.255 +  _createConfigFiles: function(aDir) {
   1.256 +    // ${InstallDir}/webapp.json
   1.257 +    yield writeToFile(OS.Path.join(aDir, this.configJson),
   1.258 +                      JSON.stringify(this.webappJson));
   1.259 +
   1.260 +    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
   1.261 +
   1.262 +    // ${InstallDir}/webapp.ini
   1.263 +    let webappINIfile = getFile(aDir, this.webappINI);
   1.264 +
   1.265 +    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
   1.266 +                 getService(Ci.nsIINIParserFactory).
   1.267 +                 createINIParser(webappINIfile).
   1.268 +                 QueryInterface(Ci.nsIINIParserWriter);
   1.269 +    writer.setString("Webapp", "Name", this.appName);
   1.270 +    writer.setString("Webapp", "Profile", this.uniqueName);
   1.271 +    writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1));
   1.272 +    writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
   1.273 +    writer.writeFile();
   1.274 +  },
   1.275 +
   1.276 +  _createSystemFiles: function(aInstallDir) {
   1.277 +    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
   1.278 +
   1.279 +    let webapprtPath = OS.Path.join(aInstallDir, this.webapprt);
   1.280 +
   1.281 +    // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop
   1.282 +    let desktopINIfile = getFile(this.desktopINI);
   1.283 +    if (desktopINIfile.parent && !desktopINIfile.parent.exists()) {
   1.284 +      desktopINIfile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
   1.285 +    }
   1.286 +
   1.287 +    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
   1.288 +                 getService(Ci.nsIINIParserFactory).
   1.289 +                 createINIParser(desktopINIfile).
   1.290 +                 QueryInterface(Ci.nsIINIParserWriter);
   1.291 +    writer.setString("Desktop Entry", "Name", this.appName);
   1.292 +    writer.setString("Desktop Entry", "Comment", this.shortDescription);
   1.293 +    writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"');
   1.294 +    writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir,
   1.295 +                                                           this.iconFile));
   1.296 +    writer.setString("Desktop Entry", "Type", "Application");
   1.297 +    writer.setString("Desktop Entry", "Terminal", "false");
   1.298 +
   1.299 +    let categories = this._translateCategories();
   1.300 +    if (categories)
   1.301 +      writer.setString("Desktop Entry", "Categories", categories);
   1.302 +
   1.303 +    writer.setString("Desktop Entry", "Actions", "Uninstall;");
   1.304 +    writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label"));
   1.305 +    writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove");
   1.306 +
   1.307 +    writer.writeFile();
   1.308 +
   1.309 +    desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR;
   1.310 +  },
   1.311 +
   1.312 +  /**
   1.313 +   * Process the icon from the imageStream as retrieved from
   1.314 +   * the URL by getIconForApp().
   1.315 +   *
   1.316 +   * @param aMimeType     ahe icon mimetype
   1.317 +   * @param aImageStream  the stream for the image data
   1.318 +   * @param aDir          the directory where the icon should be stored
   1.319 +   */
   1.320 +  _processIcon: function(aMimeType, aImageStream, aDir) {
   1.321 +    let deferred = Promise.defer();
   1.322 +
   1.323 +    let imgTools = Cc["@mozilla.org/image/tools;1"].
   1.324 +                   createInstance(Ci.imgITools);
   1.325 +
   1.326 +    let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
   1.327 +    let iconStream = imgTools.encodeImage(imgContainer, "image/png");
   1.328 +
   1.329 +    let iconFile = getFile(aDir, this.iconFile);
   1.330 +    let outputStream = FileUtils.openSafeFileOutputStream(iconFile);
   1.331 +    NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
   1.332 +      if (Components.isSuccessCode(aResult)) {
   1.333 +        deferred.resolve();
   1.334 +      } else {
   1.335 +        deferred.reject("Failure copying icon: " + aResult);
   1.336 +      }
   1.337 +    });
   1.338 +
   1.339 +    return deferred.promise;
   1.340 +  }
   1.341 +}

mercurial