1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/webapps/NativeApp.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,410 @@ 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 +this.EXPORTED_SYMBOLS = ["NativeApp"]; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.18 +Cu.import("resource://gre/modules/osfile.jsm"); 1.19 +Cu.import("resource://gre/modules/WebappOSUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.21 +Cu.import("resource://gre/modules/Task.jsm"); 1.22 +Cu.import("resource://gre/modules/Promise.jsm"); 1.23 + 1.24 +const ERR_NOT_INSTALLED = "The application isn't installed"; 1.25 +const ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME = 1.26 + "Updates for apps installed with the old naming scheme unsupported"; 1.27 + 1.28 +// 0755 1.29 +const PERMS_DIRECTORY = OS.Constants.libc.S_IRWXU | 1.30 + OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IXGRP | 1.31 + OS.Constants.libc.S_IROTH | OS.Constants.libc.S_IXOTH; 1.32 + 1.33 +// 0644 1.34 +const PERMS_FILE = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR | 1.35 + OS.Constants.libc.S_IRGRP | 1.36 + OS.Constants.libc.S_IROTH; 1.37 + 1.38 +const DESKTOP_DIR = OS.Constants.Path.desktopDir; 1.39 +const HOME_DIR = OS.Constants.Path.homeDir; 1.40 +const TMP_DIR = OS.Constants.Path.tmpDir; 1.41 + 1.42 +/** 1.43 + * This function implements the common constructor for 1.44 + * the Windows, Mac and Linux native app shells. It sets 1.45 + * the app unique name. It's meant to be called as 1.46 + * CommonNativeApp.call(this, ...) from the platform-specific 1.47 + * constructor. 1.48 + * 1.49 + * @param aApp {Object} the app object provided to the install function 1.50 + * @param aManifest {Object} the manifest data provided by the web app 1.51 + * @param aCategories {Array} array of app categories 1.52 + * @param aRegistryDir {String} (optional) path to the registry 1.53 + * 1.54 + */ 1.55 +function CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) { 1.56 + let manifest = new ManifestHelper(aManifest, aApp.origin); 1.57 + 1.58 + aApp.name = manifest.name; 1.59 + this.uniqueName = WebappOSUtils.getUniqueName(aApp); 1.60 + 1.61 + this.appName = sanitize(manifest.name); 1.62 + this.appNameAsFilename = stripStringForFilename(this.appName); 1.63 + 1.64 + if (aApp.updateManifest) { 1.65 + this.isPackaged = true; 1.66 + } 1.67 + 1.68 + this.categories = aCategories.slice(0); 1.69 + 1.70 + this.registryDir = aRegistryDir || OS.Constants.Path.profileDir; 1.71 + 1.72 + this.app = aApp; 1.73 + 1.74 + this._dryRun = false; 1.75 + try { 1.76 + if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) { 1.77 + this._dryRun = true; 1.78 + } 1.79 + } catch (ex) {} 1.80 +} 1.81 + 1.82 +CommonNativeApp.prototype = { 1.83 + uniqueName: null, 1.84 + appName: null, 1.85 + appNameAsFilename: null, 1.86 + iconURI: null, 1.87 + developerName: null, 1.88 + shortDescription: null, 1.89 + categories: null, 1.90 + webappJson: null, 1.91 + runtimeFolder: null, 1.92 + manifest: null, 1.93 + registryDir: null, 1.94 + 1.95 + /** 1.96 + * This function reads and parses the data from the app 1.97 + * manifest and stores it in the NativeApp object. 1.98 + * 1.99 + * @param aManifest {Object} the manifest data provided by the web app 1.100 + * 1.101 + */ 1.102 + _setData: function(aManifest) { 1.103 + let manifest = new ManifestHelper(aManifest, this.app.origin); 1.104 + let origin = Services.io.newURI(this.app.origin, null, null); 1.105 + 1.106 + let biggestIcon = getBiggestIconURL(manifest.icons); 1.107 + try { 1.108 + let iconURI = Services.io.newURI(biggestIcon, null, null); 1.109 + if (iconURI.scheme == "data") { 1.110 + this.iconURI = iconURI; 1.111 + } 1.112 + } catch (ex) {} 1.113 + 1.114 + if (!this.iconURI) { 1.115 + try { 1.116 + this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null); 1.117 + } 1.118 + catch (ex) {} 1.119 + } 1.120 + 1.121 + if (manifest.developer) { 1.122 + if (manifest.developer.name) { 1.123 + let devName = sanitize(manifest.developer.name.substr(0, 128)); 1.124 + if (devName) { 1.125 + this.developerName = devName; 1.126 + } 1.127 + } 1.128 + 1.129 + if (manifest.developer.url) { 1.130 + this.developerUrl = manifest.developer.url; 1.131 + } 1.132 + } 1.133 + 1.134 + if (manifest.description) { 1.135 + let firstLine = manifest.description.split("\n")[0]; 1.136 + let shortDesc = firstLine.length <= 256 1.137 + ? firstLine 1.138 + : firstLine.substr(0, 253) + "…"; 1.139 + this.shortDescription = sanitize(shortDesc); 1.140 + } else { 1.141 + this.shortDescription = this.appName; 1.142 + } 1.143 + 1.144 + if (manifest.version) { 1.145 + this.version = manifest.version; 1.146 + } 1.147 + 1.148 + this.webappJson = { 1.149 + // The app registry is the Firefox profile from which the app 1.150 + // was installed. 1.151 + "registryDir": this.registryDir, 1.152 + "app": { 1.153 + "manifest": aManifest, 1.154 + "origin": this.app.origin, 1.155 + "manifestURL": this.app.manifestURL, 1.156 + "installOrigin": this.app.installOrigin, 1.157 + "categories": this.categories, 1.158 + "receipts": this.app.receipts, 1.159 + "installTime": this.app.installTime, 1.160 + } 1.161 + }; 1.162 + 1.163 + if (this.app.etag) { 1.164 + this.webappJson.app.etag = this.app.etag; 1.165 + } 1.166 + 1.167 + if (this.app.packageEtag) { 1.168 + this.webappJson.app.packageEtag = this.app.packageEtag; 1.169 + } 1.170 + 1.171 + if (this.app.updateManifest) { 1.172 + this.webappJson.app.updateManifest = this.app.updateManifest; 1.173 + } 1.174 + 1.175 + this.runtimeFolder = OS.Constants.Path.libDir; 1.176 + }, 1.177 + 1.178 + /** 1.179 + * This function retrieves the icon for an app. 1.180 + * If the retrieving fails, it uses the default chrome icon. 1.181 + */ 1.182 + _getIcon: function(aTmpDir) { 1.183 + try { 1.184 + // If the icon is in the zip package, we should modify the url 1.185 + // to point to the zip file (we can't use the app protocol yet 1.186 + // because the app isn't installed yet). 1.187 + if (this.iconURI.scheme == "app") { 1.188 + let zipUrl = OS.Path.toFileURI(OS.Path.join(aTmpDir, 1.189 + this.zipFile)); 1.190 + 1.191 + let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath; 1.192 + 1.193 + this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath, 1.194 + null, null); 1.195 + } 1.196 + 1.197 + 1.198 + let [ mimeType, icon ] = yield downloadIcon(this.iconURI); 1.199 + yield this._processIcon(mimeType, icon, aTmpDir); 1.200 + } 1.201 + catch(e) { 1.202 + Cu.reportError("Failure retrieving icon: " + e); 1.203 + 1.204 + let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null); 1.205 + 1.206 + let [ mimeType, icon ] = yield downloadIcon(iconURI); 1.207 + yield this._processIcon(mimeType, icon, aTmpDir); 1.208 + 1.209 + // Set the iconURI property so that the user notification will have the 1.210 + // correct icon. 1.211 + this.iconURI = iconURI; 1.212 + } 1.213 + }, 1.214 + 1.215 + /** 1.216 + * Creates the profile to be used for this app. 1.217 + */ 1.218 + createProfile: function() { 1.219 + if (this._dryRun) { 1.220 + return null; 1.221 + } 1.222 + 1.223 + let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]. 1.224 + getService(Ci.nsIToolkitProfileService); 1.225 + 1.226 + try { 1.227 + let appProfile = profSvc.createDefaultProfileForApp(this.uniqueName, 1.228 + null, null); 1.229 + return appProfile.localDir; 1.230 + } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) { 1.231 + return null; 1.232 + } 1.233 + }, 1.234 +}; 1.235 + 1.236 +#ifdef XP_WIN 1.237 + 1.238 +#include WinNativeApp.js 1.239 + 1.240 +#elifdef XP_MACOSX 1.241 + 1.242 +#include MacNativeApp.js 1.243 + 1.244 +#elifdef XP_UNIX 1.245 + 1.246 +#include LinuxNativeApp.js 1.247 + 1.248 +#endif 1.249 + 1.250 +/* Helper Functions */ 1.251 + 1.252 +/** 1.253 + * Async write a data string into a file 1.254 + * 1.255 + * @param aPath the path to the file to write to 1.256 + * @param aData a string with the data to be written 1.257 + */ 1.258 +function writeToFile(aPath, aData) { 1.259 + return Task.spawn(function() { 1.260 + let data = new TextEncoder().encode(aData); 1.261 + 1.262 + let file; 1.263 + try { 1.264 + file = yield OS.File.open(aPath, { truncate: true, write: true }, 1.265 + { unixMode: PERMS_FILE }); 1.266 + yield file.write(data); 1.267 + } finally { 1.268 + yield file.close(); 1.269 + } 1.270 + }); 1.271 +} 1.272 + 1.273 +/** 1.274 + * Removes unprintable characters from a string. 1.275 + */ 1.276 +function sanitize(aStr) { 1.277 + let unprintableRE = new RegExp("[\\x00-\\x1F\\x7F]" ,"gi"); 1.278 + return aStr.replace(unprintableRE, ""); 1.279 +} 1.280 + 1.281 +/** 1.282 + * Strips all non-word characters from the beginning and end of a string. 1.283 + * Strips invalid characters from the string. 1.284 + * 1.285 + */ 1.286 +function stripStringForFilename(aPossiblyBadFilenameString) { 1.287 + // Strip everything from the front up to the first [0-9a-zA-Z] 1.288 + let stripFrontRE = new RegExp("^\\W*", "gi"); 1.289 + 1.290 + // Strip white space characters starting from the last [0-9a-zA-Z] 1.291 + let stripBackRE = new RegExp("\\s*$", "gi"); 1.292 + 1.293 + // Strip invalid characters from the filename 1.294 + let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); 1.295 + 1.296 + let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, ""); 1.297 + stripped = stripped.replace(stripBackRE, ""); 1.298 + stripped = stripped.replace(filenameRE, ""); 1.299 + 1.300 + // If the filename ends up empty, let's call it "webapp". 1.301 + if (stripped == "") { 1.302 + stripped = "webapp"; 1.303 + } 1.304 + 1.305 + return stripped; 1.306 +} 1.307 + 1.308 +/** 1.309 + * Finds a unique name available in a folder (i.e., non-existent file) 1.310 + * 1.311 + * @param aPathSet a set of paths that represents the set of 1.312 + * directories where we want to write 1.313 + * @param aName string with the filename (minus the extension) desired 1.314 + * @param aExtension string with the file extension, including the dot 1.315 + 1.316 + * @return file name or null if folder is unwritable or unique name 1.317 + * was not available 1.318 + */ 1.319 +function getAvailableFileName(aPathSet, aName, aExtension) { 1.320 + return Task.spawn(function*() { 1.321 + let name = aName + aExtension; 1.322 + 1.323 + function checkUnique(aName) { 1.324 + return Task.spawn(function*() { 1.325 + for (let path of aPathSet) { 1.326 + if (yield OS.File.exists(OS.Path.join(path, aName))) { 1.327 + return false; 1.328 + } 1.329 + } 1.330 + 1.331 + return true; 1.332 + }); 1.333 + } 1.334 + 1.335 + if (yield checkUnique(name)) { 1.336 + return name; 1.337 + } 1.338 + 1.339 + // If we're here, the plain name wasn't enough. Let's try modifying the name 1.340 + // by adding "(" + num + ")". 1.341 + for (let i = 2; i < 100; i++) { 1.342 + name = aName + " (" + i + ")" + aExtension; 1.343 + 1.344 + if (yield checkUnique(name)) { 1.345 + return name; 1.346 + } 1.347 + } 1.348 + 1.349 + throw "No available filename"; 1.350 + }); 1.351 +} 1.352 + 1.353 +/** 1.354 + * Attempts to remove files or directories. 1.355 + * 1.356 + * @param aPaths An array with paths to files to remove 1.357 + */ 1.358 +function removeFiles(aPaths) { 1.359 + for (let path of aPaths) { 1.360 + let file = getFile(path); 1.361 + 1.362 + try { 1.363 + if (file.exists()) { 1.364 + file.followLinks = false; 1.365 + file.remove(true); 1.366 + } 1.367 + } catch(ex) {} 1.368 + } 1.369 +} 1.370 + 1.371 +/** 1.372 + * Move (overwriting) the contents of one directory into another. 1.373 + * 1.374 + * @param srcPath A path to the source directory 1.375 + * @param destPath A path to the destination directory 1.376 + */ 1.377 +function moveDirectory(srcPath, destPath) { 1.378 + let srcDir = getFile(srcPath); 1.379 + let destDir = getFile(destPath); 1.380 + 1.381 + let entries = srcDir.directoryEntries; 1.382 + let array = []; 1.383 + while (entries.hasMoreElements()) { 1.384 + let entry = entries.getNext().QueryInterface(Ci.nsIFile); 1.385 + if (entry.isDirectory()) { 1.386 + yield moveDirectory(entry.path, OS.Path.join(destPath, entry.leafName)); 1.387 + } else { 1.388 + entry.moveTo(destDir, entry.leafName); 1.389 + } 1.390 + } 1.391 + 1.392 + // The source directory is now empty, remove it. 1.393 + yield OS.File.removeEmptyDir(srcPath); 1.394 +} 1.395 + 1.396 +function escapeXML(aStr) { 1.397 + return aStr.toString() 1.398 + .replace(/&/g, "&") 1.399 + .replace(/"/g, """) 1.400 + .replace(/'/g, "'") 1.401 + .replace(/</g, "<") 1.402 + .replace(/>/g, ">"); 1.403 +} 1.404 + 1.405 +// Helper to create a nsIFile from a set of path components 1.406 +function getFile() { 1.407 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.408 + file.initWithPath(OS.Path.join.apply(OS.Path, arguments)); 1.409 + return file; 1.410 +} 1.411 + 1.412 +/* More helpers for handling the app icon */ 1.413 +#include WebappsIconHelpers.js