michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Constructor for the Linux native app shell michael@0: * michael@0: * @param aApp {Object} the app object provided to the install function michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aCategories {Array} array of app categories michael@0: * @param aRegistryDir {String} (optional) path to the registry michael@0: */ michael@0: function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { michael@0: CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); michael@0: michael@0: this.iconFile = "icon.png"; michael@0: this.webapprt = "webapprt-stub"; michael@0: this.configJson = "webapp.json"; michael@0: this.webappINI = "webapp.ini"; michael@0: this.zipFile = "application.zip"; michael@0: michael@0: this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ]; michael@0: if (this.isPackaged) { michael@0: this.backupFiles.push(this.zipFile); michael@0: } michael@0: michael@0: let xdg_data_home = Cc["@mozilla.org/process/environment;1"]. michael@0: getService(Ci.nsIEnvironment). michael@0: get("XDG_DATA_HOME"); michael@0: if (!xdg_data_home) { michael@0: xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share"); michael@0: } michael@0: michael@0: // The desktop file name is: "owa-" + sanitized app name + michael@0: // "-" + manifest url hash. michael@0: this.desktopINI = OS.Path.join(xdg_data_home, "applications", michael@0: "owa-" + this.uniqueName + ".desktop"); michael@0: } michael@0: michael@0: NativeApp.prototype = { michael@0: __proto__: CommonNativeApp.prototype, michael@0: michael@0: /** michael@0: * Creates a native installation of the web app in the OS michael@0: * michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aZipPath {String} path to the zip file for packaged apps (undefined michael@0: * for hosted apps) michael@0: */ michael@0: install: Task.async(function*(aManifest, aZipPath) { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: // If the application is already installed, this is a reinstallation. michael@0: if (WebappOSUtils.getInstallPath(this.app)) { michael@0: return yield this.prepareUpdate(aManifest, aZipPath); michael@0: } michael@0: michael@0: this._setData(aManifest); michael@0: michael@0: // The installation directory name is: sanitized app name + michael@0: // "-" + manifest url hash. michael@0: let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName); michael@0: michael@0: let dir = getFile(TMP_DIR, this.uniqueName); michael@0: dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); michael@0: let tmpDir = dir.path; michael@0: michael@0: // Create the installation in a temporary directory. michael@0: try { michael@0: this._copyPrebuiltFiles(tmpDir); michael@0: yield this._createConfigFiles(tmpDir); michael@0: michael@0: if (aZipPath) { michael@0: yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); michael@0: } michael@0: michael@0: yield this._getIcon(tmpDir); michael@0: } catch (ex) { michael@0: yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: michael@0: // Apply the installation. michael@0: this._removeInstallation(true, installDir); michael@0: michael@0: try { michael@0: yield this._applyTempInstallation(tmpDir, installDir); michael@0: } catch (ex) { michael@0: this._removeInstallation(false, installDir); michael@0: yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Creates an update in a temporary directory to be applied later. michael@0: * michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * @param aZipPath {String} path to the zip file for packaged apps (undefined michael@0: * for hosted apps) michael@0: */ michael@0: prepareUpdate: Task.async(function*(aManifest, aZipPath) { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: this._setData(aManifest); michael@0: michael@0: let installDir = WebappOSUtils.getInstallPath(this.app); michael@0: if (!installDir) { michael@0: throw ERR_NOT_INSTALLED; michael@0: } michael@0: michael@0: let baseName = OS.Path.basename(installDir) michael@0: let oldUniqueName = baseName.substring(1, baseName.length); michael@0: if (this.uniqueName != oldUniqueName) { michael@0: // Bug 919799: If the app is still in the registry, migrate its data to michael@0: // the new format. michael@0: throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; michael@0: } michael@0: michael@0: let updateDir = OS.Path.join(installDir, "update"); michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: yield OS.File.makeDir(updateDir); michael@0: michael@0: try { michael@0: yield this._createConfigFiles(updateDir); michael@0: michael@0: if (aZipPath) { michael@0: yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); michael@0: } michael@0: michael@0: yield this._getIcon(updateDir); michael@0: } catch (ex) { michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: throw ex; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Applies an update. michael@0: */ michael@0: applyUpdate: Task.async(function*() { michael@0: if (this._dryRun) { michael@0: return; michael@0: } michael@0: michael@0: let installDir = WebappOSUtils.getInstallPath(this.app); michael@0: let updateDir = OS.Path.join(installDir, "update"); michael@0: michael@0: let backupDir = yield this._backupInstallation(installDir); michael@0: michael@0: try { michael@0: yield this._applyTempInstallation(updateDir, installDir); michael@0: } catch (ex) { michael@0: yield this._restoreInstallation(backupDir, installDir); michael@0: throw ex; michael@0: } finally { michael@0: yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); michael@0: yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); michael@0: } michael@0: }), michael@0: michael@0: _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { michael@0: yield moveDirectory(aTmpDir, aInstallDir); michael@0: michael@0: this._createSystemFiles(aInstallDir); michael@0: }), michael@0: michael@0: _removeInstallation: function(keepProfile, aInstallDir) { michael@0: let filesToRemove = [this.desktopINI]; michael@0: michael@0: if (keepProfile) { michael@0: for (let filePath of this.backupFiles) { michael@0: filesToRemove.push(OS.Path.join(aInstallDir, filePath)); michael@0: } michael@0: michael@0: filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); michael@0: } else { michael@0: filesToRemove.push(aInstallDir); michael@0: } michael@0: michael@0: removeFiles(filesToRemove); michael@0: }, michael@0: michael@0: _backupInstallation: Task.async(function*(aInstallDir) { michael@0: let backupDir = OS.Path.join(aInstallDir, "backup"); michael@0: yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); michael@0: yield OS.File.makeDir(backupDir); michael@0: michael@0: for (let filePath of this.backupFiles) { michael@0: yield OS.File.move(OS.Path.join(aInstallDir, filePath), michael@0: OS.Path.join(backupDir, filePath)); michael@0: } michael@0: michael@0: return backupDir; michael@0: }), michael@0: michael@0: _restoreInstallation: function(aBackupDir, aInstallDir) { michael@0: return moveDirectory(aBackupDir, aInstallDir); michael@0: }, michael@0: michael@0: _copyPrebuiltFiles: function(aDir) { michael@0: let destDir = getFile(aDir); michael@0: let stub = getFile(this.runtimeFolder, this.webapprt); michael@0: stub.copyTo(destDir, null); michael@0: }, michael@0: michael@0: /** michael@0: * Translate marketplace categories to freedesktop.org categories. michael@0: * michael@0: * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry michael@0: * michael@0: * @return an array of categories michael@0: */ michael@0: _translateCategories: function() { michael@0: let translations = { michael@0: "books": "Education;Literature", michael@0: "business": "Finance", michael@0: "education": "Education", michael@0: "entertainment": "Amusement", michael@0: "sports": "Sports", michael@0: "games": "Game", michael@0: "health-fitness": "MedicalSoftware", michael@0: "lifestyle": "Amusement", michael@0: "music": "Audio;Music", michael@0: "news-weather": "News", michael@0: "photo-video": "Video;AudioVideo;Photography", michael@0: "productivity": "Office", michael@0: "shopping": "Amusement", michael@0: "social": "Chat", michael@0: "travel": "Amusement", michael@0: "reference": "Science;Education;Documentation", michael@0: "maps-navigation": "Maps", michael@0: "utilities": "Utility" michael@0: }; michael@0: michael@0: // The trailing semicolon is needed as written in the freedesktop specification michael@0: let categories = ""; michael@0: for (let category of this.categories) { michael@0: let catLower = category.toLowerCase(); michael@0: if (catLower in translations) { michael@0: categories += translations[catLower] + ";"; michael@0: } michael@0: } michael@0: michael@0: return categories; michael@0: }, michael@0: michael@0: _createConfigFiles: function(aDir) { michael@0: // ${InstallDir}/webapp.json michael@0: yield writeToFile(OS.Path.join(aDir, this.configJson), michael@0: JSON.stringify(this.webappJson)); michael@0: michael@0: let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); michael@0: michael@0: // ${InstallDir}/webapp.ini michael@0: let webappINIfile = getFile(aDir, this.webappINI); michael@0: michael@0: let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. michael@0: getService(Ci.nsIINIParserFactory). michael@0: createINIParser(webappINIfile). michael@0: QueryInterface(Ci.nsIINIParserWriter); michael@0: writer.setString("Webapp", "Name", this.appName); michael@0: writer.setString("Webapp", "Profile", this.uniqueName); michael@0: writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1)); michael@0: writer.setString("WebappRT", "InstallDir", this.runtimeFolder); michael@0: writer.writeFile(); michael@0: }, michael@0: michael@0: _createSystemFiles: function(aInstallDir) { michael@0: let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); michael@0: michael@0: let webapprtPath = OS.Path.join(aInstallDir, this.webapprt); michael@0: michael@0: // $XDG_DATA_HOME/applications/owa-.desktop michael@0: let desktopINIfile = getFile(this.desktopINI); michael@0: if (desktopINIfile.parent && !desktopINIfile.parent.exists()) { michael@0: desktopINIfile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); michael@0: } michael@0: michael@0: let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. michael@0: getService(Ci.nsIINIParserFactory). michael@0: createINIParser(desktopINIfile). michael@0: QueryInterface(Ci.nsIINIParserWriter); michael@0: writer.setString("Desktop Entry", "Name", this.appName); michael@0: writer.setString("Desktop Entry", "Comment", this.shortDescription); michael@0: writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"'); michael@0: writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir, michael@0: this.iconFile)); michael@0: writer.setString("Desktop Entry", "Type", "Application"); michael@0: writer.setString("Desktop Entry", "Terminal", "false"); michael@0: michael@0: let categories = this._translateCategories(); michael@0: if (categories) michael@0: writer.setString("Desktop Entry", "Categories", categories); michael@0: michael@0: writer.setString("Desktop Entry", "Actions", "Uninstall;"); michael@0: writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label")); michael@0: writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove"); michael@0: michael@0: writer.writeFile(); michael@0: michael@0: desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR; michael@0: }, michael@0: michael@0: /** michael@0: * Process the icon from the imageStream as retrieved from michael@0: * the URL by getIconForApp(). michael@0: * michael@0: * @param aMimeType ahe icon mimetype michael@0: * @param aImageStream the stream for the image data michael@0: * @param aDir the directory where the icon should be stored michael@0: */ michael@0: _processIcon: function(aMimeType, aImageStream, aDir) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let imgTools = Cc["@mozilla.org/image/tools;1"]. michael@0: createInstance(Ci.imgITools); michael@0: michael@0: let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); michael@0: let iconStream = imgTools.encodeImage(imgContainer, "image/png"); michael@0: michael@0: let iconFile = getFile(aDir, this.iconFile); michael@0: let outputStream = FileUtils.openSafeFileOutputStream(iconFile); michael@0: NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { michael@0: if (Components.isSuccessCode(aResult)) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject("Failure copying icon: " + aResult); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: }