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