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: this.EXPORTED_SYMBOLS = ["NativeApp"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/WebappOSUtils.jsm"); michael@0: Cu.import("resource://gre/modules/AppsUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: const ERR_NOT_INSTALLED = "The application isn't installed"; michael@0: const ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME = michael@0: "Updates for apps installed with the old naming scheme unsupported"; michael@0: michael@0: // 0755 michael@0: const PERMS_DIRECTORY = OS.Constants.libc.S_IRWXU | michael@0: OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IXGRP | michael@0: OS.Constants.libc.S_IROTH | OS.Constants.libc.S_IXOTH; michael@0: michael@0: // 0644 michael@0: const PERMS_FILE = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR | michael@0: OS.Constants.libc.S_IRGRP | michael@0: OS.Constants.libc.S_IROTH; michael@0: michael@0: const DESKTOP_DIR = OS.Constants.Path.desktopDir; michael@0: const HOME_DIR = OS.Constants.Path.homeDir; michael@0: const TMP_DIR = OS.Constants.Path.tmpDir; michael@0: michael@0: /** michael@0: * This function implements the common constructor for michael@0: * the Windows, Mac and Linux native app shells. It sets michael@0: * the app unique name. It's meant to be called as michael@0: * CommonNativeApp.call(this, ...) from the platform-specific michael@0: * constructor. 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: */ michael@0: function CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) { michael@0: let manifest = new ManifestHelper(aManifest, aApp.origin); michael@0: michael@0: aApp.name = manifest.name; michael@0: this.uniqueName = WebappOSUtils.getUniqueName(aApp); michael@0: michael@0: this.appName = sanitize(manifest.name); michael@0: this.appNameAsFilename = stripStringForFilename(this.appName); michael@0: michael@0: if (aApp.updateManifest) { michael@0: this.isPackaged = true; michael@0: } michael@0: michael@0: this.categories = aCategories.slice(0); michael@0: michael@0: this.registryDir = aRegistryDir || OS.Constants.Path.profileDir; michael@0: michael@0: this.app = aApp; michael@0: michael@0: this._dryRun = false; michael@0: try { michael@0: if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) { michael@0: this._dryRun = true; michael@0: } michael@0: } catch (ex) {} michael@0: } michael@0: michael@0: CommonNativeApp.prototype = { michael@0: uniqueName: null, michael@0: appName: null, michael@0: appNameAsFilename: null, michael@0: iconURI: null, michael@0: developerName: null, michael@0: shortDescription: null, michael@0: categories: null, michael@0: webappJson: null, michael@0: runtimeFolder: null, michael@0: manifest: null, michael@0: registryDir: null, michael@0: michael@0: /** michael@0: * This function reads and parses the data from the app michael@0: * manifest and stores it in the NativeApp object. michael@0: * michael@0: * @param aManifest {Object} the manifest data provided by the web app michael@0: * michael@0: */ michael@0: _setData: function(aManifest) { michael@0: let manifest = new ManifestHelper(aManifest, this.app.origin); michael@0: let origin = Services.io.newURI(this.app.origin, null, null); michael@0: michael@0: let biggestIcon = getBiggestIconURL(manifest.icons); michael@0: try { michael@0: let iconURI = Services.io.newURI(biggestIcon, null, null); michael@0: if (iconURI.scheme == "data") { michael@0: this.iconURI = iconURI; michael@0: } michael@0: } catch (ex) {} michael@0: michael@0: if (!this.iconURI) { michael@0: try { michael@0: this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null); michael@0: } michael@0: catch (ex) {} michael@0: } michael@0: michael@0: if (manifest.developer) { michael@0: if (manifest.developer.name) { michael@0: let devName = sanitize(manifest.developer.name.substr(0, 128)); michael@0: if (devName) { michael@0: this.developerName = devName; michael@0: } michael@0: } michael@0: michael@0: if (manifest.developer.url) { michael@0: this.developerUrl = manifest.developer.url; michael@0: } michael@0: } michael@0: michael@0: if (manifest.description) { michael@0: let firstLine = manifest.description.split("\n")[0]; michael@0: let shortDesc = firstLine.length <= 256 michael@0: ? firstLine michael@0: : firstLine.substr(0, 253) + "…"; michael@0: this.shortDescription = sanitize(shortDesc); michael@0: } else { michael@0: this.shortDescription = this.appName; michael@0: } michael@0: michael@0: if (manifest.version) { michael@0: this.version = manifest.version; michael@0: } michael@0: michael@0: this.webappJson = { michael@0: // The app registry is the Firefox profile from which the app michael@0: // was installed. michael@0: "registryDir": this.registryDir, michael@0: "app": { michael@0: "manifest": aManifest, michael@0: "origin": this.app.origin, michael@0: "manifestURL": this.app.manifestURL, michael@0: "installOrigin": this.app.installOrigin, michael@0: "categories": this.categories, michael@0: "receipts": this.app.receipts, michael@0: "installTime": this.app.installTime, michael@0: } michael@0: }; michael@0: michael@0: if (this.app.etag) { michael@0: this.webappJson.app.etag = this.app.etag; michael@0: } michael@0: michael@0: if (this.app.packageEtag) { michael@0: this.webappJson.app.packageEtag = this.app.packageEtag; michael@0: } michael@0: michael@0: if (this.app.updateManifest) { michael@0: this.webappJson.app.updateManifest = this.app.updateManifest; michael@0: } michael@0: michael@0: this.runtimeFolder = OS.Constants.Path.libDir; michael@0: }, michael@0: michael@0: /** michael@0: * This function retrieves the icon for an app. michael@0: * If the retrieving fails, it uses the default chrome icon. michael@0: */ michael@0: _getIcon: function(aTmpDir) { michael@0: try { michael@0: // If the icon is in the zip package, we should modify the url michael@0: // to point to the zip file (we can't use the app protocol yet michael@0: // because the app isn't installed yet). michael@0: if (this.iconURI.scheme == "app") { michael@0: let zipUrl = OS.Path.toFileURI(OS.Path.join(aTmpDir, michael@0: this.zipFile)); michael@0: michael@0: let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath; michael@0: michael@0: this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath, michael@0: null, null); michael@0: } michael@0: michael@0: michael@0: let [ mimeType, icon ] = yield downloadIcon(this.iconURI); michael@0: yield this._processIcon(mimeType, icon, aTmpDir); michael@0: } michael@0: catch(e) { michael@0: Cu.reportError("Failure retrieving icon: " + e); michael@0: michael@0: let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null); michael@0: michael@0: let [ mimeType, icon ] = yield downloadIcon(iconURI); michael@0: yield this._processIcon(mimeType, icon, aTmpDir); michael@0: michael@0: // Set the iconURI property so that the user notification will have the michael@0: // correct icon. michael@0: this.iconURI = iconURI; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates the profile to be used for this app. michael@0: */ michael@0: createProfile: function() { michael@0: if (this._dryRun) { michael@0: return null; michael@0: } michael@0: michael@0: let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]. michael@0: getService(Ci.nsIToolkitProfileService); michael@0: michael@0: try { michael@0: let appProfile = profSvc.createDefaultProfileForApp(this.uniqueName, michael@0: null, null); michael@0: return appProfile.localDir; michael@0: } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: return null; michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: #ifdef XP_WIN michael@0: michael@0: #include WinNativeApp.js michael@0: michael@0: #elifdef XP_MACOSX michael@0: michael@0: #include MacNativeApp.js michael@0: michael@0: #elifdef XP_UNIX michael@0: michael@0: #include LinuxNativeApp.js michael@0: michael@0: #endif michael@0: michael@0: /* Helper Functions */ michael@0: michael@0: /** michael@0: * Async write a data string into a file michael@0: * michael@0: * @param aPath the path to the file to write to michael@0: * @param aData a string with the data to be written michael@0: */ michael@0: function writeToFile(aPath, aData) { michael@0: return Task.spawn(function() { michael@0: let data = new TextEncoder().encode(aData); michael@0: michael@0: let file; michael@0: try { michael@0: file = yield OS.File.open(aPath, { truncate: true, write: true }, michael@0: { unixMode: PERMS_FILE }); michael@0: yield file.write(data); michael@0: } finally { michael@0: yield file.close(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Removes unprintable characters from a string. michael@0: */ michael@0: function sanitize(aStr) { michael@0: let unprintableRE = new RegExp("[\\x00-\\x1F\\x7F]" ,"gi"); michael@0: return aStr.replace(unprintableRE, ""); michael@0: } michael@0: michael@0: /** michael@0: * Strips all non-word characters from the beginning and end of a string. michael@0: * Strips invalid characters from the string. michael@0: * michael@0: */ michael@0: function stripStringForFilename(aPossiblyBadFilenameString) { michael@0: // Strip everything from the front up to the first [0-9a-zA-Z] michael@0: let stripFrontRE = new RegExp("^\\W*", "gi"); michael@0: michael@0: // Strip white space characters starting from the last [0-9a-zA-Z] michael@0: let stripBackRE = new RegExp("\\s*$", "gi"); michael@0: michael@0: // Strip invalid characters from the filename michael@0: let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); michael@0: michael@0: let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, ""); michael@0: stripped = stripped.replace(stripBackRE, ""); michael@0: stripped = stripped.replace(filenameRE, ""); michael@0: michael@0: // If the filename ends up empty, let's call it "webapp". michael@0: if (stripped == "") { michael@0: stripped = "webapp"; michael@0: } michael@0: michael@0: return stripped; michael@0: } michael@0: michael@0: /** michael@0: * Finds a unique name available in a folder (i.e., non-existent file) michael@0: * michael@0: * @param aPathSet a set of paths that represents the set of michael@0: * directories where we want to write michael@0: * @param aName string with the filename (minus the extension) desired michael@0: * @param aExtension string with the file extension, including the dot michael@0: michael@0: * @return file name or null if folder is unwritable or unique name michael@0: * was not available michael@0: */ michael@0: function getAvailableFileName(aPathSet, aName, aExtension) { michael@0: return Task.spawn(function*() { michael@0: let name = aName + aExtension; michael@0: michael@0: function checkUnique(aName) { michael@0: return Task.spawn(function*() { michael@0: for (let path of aPathSet) { michael@0: if (yield OS.File.exists(OS.Path.join(path, aName))) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }); michael@0: } michael@0: michael@0: if (yield checkUnique(name)) { michael@0: return name; michael@0: } michael@0: michael@0: // If we're here, the plain name wasn't enough. Let's try modifying the name michael@0: // by adding "(" + num + ")". michael@0: for (let i = 2; i < 100; i++) { michael@0: name = aName + " (" + i + ")" + aExtension; michael@0: michael@0: if (yield checkUnique(name)) { michael@0: return name; michael@0: } michael@0: } michael@0: michael@0: throw "No available filename"; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Attempts to remove files or directories. michael@0: * michael@0: * @param aPaths An array with paths to files to remove michael@0: */ michael@0: function removeFiles(aPaths) { michael@0: for (let path of aPaths) { michael@0: let file = getFile(path); michael@0: michael@0: try { michael@0: if (file.exists()) { michael@0: file.followLinks = false; michael@0: file.remove(true); michael@0: } michael@0: } catch(ex) {} michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move (overwriting) the contents of one directory into another. michael@0: * michael@0: * @param srcPath A path to the source directory michael@0: * @param destPath A path to the destination directory michael@0: */ michael@0: function moveDirectory(srcPath, destPath) { michael@0: let srcDir = getFile(srcPath); michael@0: let destDir = getFile(destPath); michael@0: michael@0: let entries = srcDir.directoryEntries; michael@0: let array = []; michael@0: while (entries.hasMoreElements()) { michael@0: let entry = entries.getNext().QueryInterface(Ci.nsIFile); michael@0: if (entry.isDirectory()) { michael@0: yield moveDirectory(entry.path, OS.Path.join(destPath, entry.leafName)); michael@0: } else { michael@0: entry.moveTo(destDir, entry.leafName); michael@0: } michael@0: } michael@0: michael@0: // The source directory is now empty, remove it. michael@0: yield OS.File.removeEmptyDir(srcPath); michael@0: } michael@0: michael@0: function escapeXML(aStr) { michael@0: return aStr.toString() michael@0: .replace(/&/g, "&") michael@0: .replace(/"/g, """) michael@0: .replace(/'/g, "'") michael@0: .replace(//g, ">"); michael@0: } michael@0: michael@0: // Helper to create a nsIFile from a set of path components michael@0: function getFile() { michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.initWithPath(OS.Path.join.apply(OS.Path, arguments)); michael@0: return file; michael@0: } michael@0: michael@0: /* More helpers for handling the app icon */ michael@0: #include WebappsIconHelpers.js