Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 this.EXPORTED_SYMBOLS = ["NativeApp"];
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10 const Cr = Components.results;
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/FileUtils.jsm");
14 Cu.import("resource://gre/modules/NetUtil.jsm");
15 Cu.import("resource://gre/modules/osfile.jsm");
16 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
17 Cu.import("resource://gre/modules/AppsUtils.jsm");
18 Cu.import("resource://gre/modules/Task.jsm");
19 Cu.import("resource://gre/modules/Promise.jsm");
21 const ERR_NOT_INSTALLED = "The application isn't installed";
22 const ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME =
23 "Updates for apps installed with the old naming scheme unsupported";
25 // 0755
26 const PERMS_DIRECTORY = OS.Constants.libc.S_IRWXU |
27 OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IXGRP |
28 OS.Constants.libc.S_IROTH | OS.Constants.libc.S_IXOTH;
30 // 0644
31 const PERMS_FILE = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR |
32 OS.Constants.libc.S_IRGRP |
33 OS.Constants.libc.S_IROTH;
35 const DESKTOP_DIR = OS.Constants.Path.desktopDir;
36 const HOME_DIR = OS.Constants.Path.homeDir;
37 const TMP_DIR = OS.Constants.Path.tmpDir;
39 /**
40 * This function implements the common constructor for
41 * the Windows, Mac and Linux native app shells. It sets
42 * the app unique name. It's meant to be called as
43 * CommonNativeApp.call(this, ...) from the platform-specific
44 * constructor.
45 *
46 * @param aApp {Object} the app object provided to the install function
47 * @param aManifest {Object} the manifest data provided by the web app
48 * @param aCategories {Array} array of app categories
49 * @param aRegistryDir {String} (optional) path to the registry
50 *
51 */
52 function CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) {
53 let manifest = new ManifestHelper(aManifest, aApp.origin);
55 aApp.name = manifest.name;
56 this.uniqueName = WebappOSUtils.getUniqueName(aApp);
58 this.appName = sanitize(manifest.name);
59 this.appNameAsFilename = stripStringForFilename(this.appName);
61 if (aApp.updateManifest) {
62 this.isPackaged = true;
63 }
65 this.categories = aCategories.slice(0);
67 this.registryDir = aRegistryDir || OS.Constants.Path.profileDir;
69 this.app = aApp;
71 this._dryRun = false;
72 try {
73 if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
74 this._dryRun = true;
75 }
76 } catch (ex) {}
77 }
79 CommonNativeApp.prototype = {
80 uniqueName: null,
81 appName: null,
82 appNameAsFilename: null,
83 iconURI: null,
84 developerName: null,
85 shortDescription: null,
86 categories: null,
87 webappJson: null,
88 runtimeFolder: null,
89 manifest: null,
90 registryDir: null,
92 /**
93 * This function reads and parses the data from the app
94 * manifest and stores it in the NativeApp object.
95 *
96 * @param aManifest {Object} the manifest data provided by the web app
97 *
98 */
99 _setData: function(aManifest) {
100 let manifest = new ManifestHelper(aManifest, this.app.origin);
101 let origin = Services.io.newURI(this.app.origin, null, null);
103 let biggestIcon = getBiggestIconURL(manifest.icons);
104 try {
105 let iconURI = Services.io.newURI(biggestIcon, null, null);
106 if (iconURI.scheme == "data") {
107 this.iconURI = iconURI;
108 }
109 } catch (ex) {}
111 if (!this.iconURI) {
112 try {
113 this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null);
114 }
115 catch (ex) {}
116 }
118 if (manifest.developer) {
119 if (manifest.developer.name) {
120 let devName = sanitize(manifest.developer.name.substr(0, 128));
121 if (devName) {
122 this.developerName = devName;
123 }
124 }
126 if (manifest.developer.url) {
127 this.developerUrl = manifest.developer.url;
128 }
129 }
131 if (manifest.description) {
132 let firstLine = manifest.description.split("\n")[0];
133 let shortDesc = firstLine.length <= 256
134 ? firstLine
135 : firstLine.substr(0, 253) + "…";
136 this.shortDescription = sanitize(shortDesc);
137 } else {
138 this.shortDescription = this.appName;
139 }
141 if (manifest.version) {
142 this.version = manifest.version;
143 }
145 this.webappJson = {
146 // The app registry is the Firefox profile from which the app
147 // was installed.
148 "registryDir": this.registryDir,
149 "app": {
150 "manifest": aManifest,
151 "origin": this.app.origin,
152 "manifestURL": this.app.manifestURL,
153 "installOrigin": this.app.installOrigin,
154 "categories": this.categories,
155 "receipts": this.app.receipts,
156 "installTime": this.app.installTime,
157 }
158 };
160 if (this.app.etag) {
161 this.webappJson.app.etag = this.app.etag;
162 }
164 if (this.app.packageEtag) {
165 this.webappJson.app.packageEtag = this.app.packageEtag;
166 }
168 if (this.app.updateManifest) {
169 this.webappJson.app.updateManifest = this.app.updateManifest;
170 }
172 this.runtimeFolder = OS.Constants.Path.libDir;
173 },
175 /**
176 * This function retrieves the icon for an app.
177 * If the retrieving fails, it uses the default chrome icon.
178 */
179 _getIcon: function(aTmpDir) {
180 try {
181 // If the icon is in the zip package, we should modify the url
182 // to point to the zip file (we can't use the app protocol yet
183 // because the app isn't installed yet).
184 if (this.iconURI.scheme == "app") {
185 let zipUrl = OS.Path.toFileURI(OS.Path.join(aTmpDir,
186 this.zipFile));
188 let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath;
190 this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath,
191 null, null);
192 }
195 let [ mimeType, icon ] = yield downloadIcon(this.iconURI);
196 yield this._processIcon(mimeType, icon, aTmpDir);
197 }
198 catch(e) {
199 Cu.reportError("Failure retrieving icon: " + e);
201 let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null);
203 let [ mimeType, icon ] = yield downloadIcon(iconURI);
204 yield this._processIcon(mimeType, icon, aTmpDir);
206 // Set the iconURI property so that the user notification will have the
207 // correct icon.
208 this.iconURI = iconURI;
209 }
210 },
212 /**
213 * Creates the profile to be used for this app.
214 */
215 createProfile: function() {
216 if (this._dryRun) {
217 return null;
218 }
220 let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].
221 getService(Ci.nsIToolkitProfileService);
223 try {
224 let appProfile = profSvc.createDefaultProfileForApp(this.uniqueName,
225 null, null);
226 return appProfile.localDir;
227 } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
228 return null;
229 }
230 },
231 };
233 #ifdef XP_WIN
235 #include WinNativeApp.js
237 #elifdef XP_MACOSX
239 #include MacNativeApp.js
241 #elifdef XP_UNIX
243 #include LinuxNativeApp.js
245 #endif
247 /* Helper Functions */
249 /**
250 * Async write a data string into a file
251 *
252 * @param aPath the path to the file to write to
253 * @param aData a string with the data to be written
254 */
255 function writeToFile(aPath, aData) {
256 return Task.spawn(function() {
257 let data = new TextEncoder().encode(aData);
259 let file;
260 try {
261 file = yield OS.File.open(aPath, { truncate: true, write: true },
262 { unixMode: PERMS_FILE });
263 yield file.write(data);
264 } finally {
265 yield file.close();
266 }
267 });
268 }
270 /**
271 * Removes unprintable characters from a string.
272 */
273 function sanitize(aStr) {
274 let unprintableRE = new RegExp("[\\x00-\\x1F\\x7F]" ,"gi");
275 return aStr.replace(unprintableRE, "");
276 }
278 /**
279 * Strips all non-word characters from the beginning and end of a string.
280 * Strips invalid characters from the string.
281 *
282 */
283 function stripStringForFilename(aPossiblyBadFilenameString) {
284 // Strip everything from the front up to the first [0-9a-zA-Z]
285 let stripFrontRE = new RegExp("^\\W*", "gi");
287 // Strip white space characters starting from the last [0-9a-zA-Z]
288 let stripBackRE = new RegExp("\\s*$", "gi");
290 // Strip invalid characters from the filename
291 let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
293 let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, "");
294 stripped = stripped.replace(stripBackRE, "");
295 stripped = stripped.replace(filenameRE, "");
297 // If the filename ends up empty, let's call it "webapp".
298 if (stripped == "") {
299 stripped = "webapp";
300 }
302 return stripped;
303 }
305 /**
306 * Finds a unique name available in a folder (i.e., non-existent file)
307 *
308 * @param aPathSet a set of paths that represents the set of
309 * directories where we want to write
310 * @param aName string with the filename (minus the extension) desired
311 * @param aExtension string with the file extension, including the dot
313 * @return file name or null if folder is unwritable or unique name
314 * was not available
315 */
316 function getAvailableFileName(aPathSet, aName, aExtension) {
317 return Task.spawn(function*() {
318 let name = aName + aExtension;
320 function checkUnique(aName) {
321 return Task.spawn(function*() {
322 for (let path of aPathSet) {
323 if (yield OS.File.exists(OS.Path.join(path, aName))) {
324 return false;
325 }
326 }
328 return true;
329 });
330 }
332 if (yield checkUnique(name)) {
333 return name;
334 }
336 // If we're here, the plain name wasn't enough. Let's try modifying the name
337 // by adding "(" + num + ")".
338 for (let i = 2; i < 100; i++) {
339 name = aName + " (" + i + ")" + aExtension;
341 if (yield checkUnique(name)) {
342 return name;
343 }
344 }
346 throw "No available filename";
347 });
348 }
350 /**
351 * Attempts to remove files or directories.
352 *
353 * @param aPaths An array with paths to files to remove
354 */
355 function removeFiles(aPaths) {
356 for (let path of aPaths) {
357 let file = getFile(path);
359 try {
360 if (file.exists()) {
361 file.followLinks = false;
362 file.remove(true);
363 }
364 } catch(ex) {}
365 }
366 }
368 /**
369 * Move (overwriting) the contents of one directory into another.
370 *
371 * @param srcPath A path to the source directory
372 * @param destPath A path to the destination directory
373 */
374 function moveDirectory(srcPath, destPath) {
375 let srcDir = getFile(srcPath);
376 let destDir = getFile(destPath);
378 let entries = srcDir.directoryEntries;
379 let array = [];
380 while (entries.hasMoreElements()) {
381 let entry = entries.getNext().QueryInterface(Ci.nsIFile);
382 if (entry.isDirectory()) {
383 yield moveDirectory(entry.path, OS.Path.join(destPath, entry.leafName));
384 } else {
385 entry.moveTo(destDir, entry.leafName);
386 }
387 }
389 // The source directory is now empty, remove it.
390 yield OS.File.removeEmptyDir(srcPath);
391 }
393 function escapeXML(aStr) {
394 return aStr.toString()
395 .replace(/&/g, "&")
396 .replace(/"/g, """)
397 .replace(/'/g, "'")
398 .replace(/</g, "<")
399 .replace(/>/g, ">");
400 }
402 // Helper to create a nsIFile from a set of path components
403 function getFile() {
404 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
405 file.initWithPath(OS.Path.join.apply(OS.Path, arguments));
406 return file;
407 }
409 /* More helpers for handling the app icon */
410 #include WebappsIconHelpers.js