|
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/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = ["NativeApp"]; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 const Cr = Components.results; |
|
11 |
|
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"); |
|
20 |
|
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"; |
|
24 |
|
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; |
|
29 |
|
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; |
|
34 |
|
35 const DESKTOP_DIR = OS.Constants.Path.desktopDir; |
|
36 const HOME_DIR = OS.Constants.Path.homeDir; |
|
37 const TMP_DIR = OS.Constants.Path.tmpDir; |
|
38 |
|
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); |
|
54 |
|
55 aApp.name = manifest.name; |
|
56 this.uniqueName = WebappOSUtils.getUniqueName(aApp); |
|
57 |
|
58 this.appName = sanitize(manifest.name); |
|
59 this.appNameAsFilename = stripStringForFilename(this.appName); |
|
60 |
|
61 if (aApp.updateManifest) { |
|
62 this.isPackaged = true; |
|
63 } |
|
64 |
|
65 this.categories = aCategories.slice(0); |
|
66 |
|
67 this.registryDir = aRegistryDir || OS.Constants.Path.profileDir; |
|
68 |
|
69 this.app = aApp; |
|
70 |
|
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 } |
|
78 |
|
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, |
|
91 |
|
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); |
|
102 |
|
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) {} |
|
110 |
|
111 if (!this.iconURI) { |
|
112 try { |
|
113 this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null); |
|
114 } |
|
115 catch (ex) {} |
|
116 } |
|
117 |
|
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 } |
|
125 |
|
126 if (manifest.developer.url) { |
|
127 this.developerUrl = manifest.developer.url; |
|
128 } |
|
129 } |
|
130 |
|
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 } |
|
140 |
|
141 if (manifest.version) { |
|
142 this.version = manifest.version; |
|
143 } |
|
144 |
|
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 }; |
|
159 |
|
160 if (this.app.etag) { |
|
161 this.webappJson.app.etag = this.app.etag; |
|
162 } |
|
163 |
|
164 if (this.app.packageEtag) { |
|
165 this.webappJson.app.packageEtag = this.app.packageEtag; |
|
166 } |
|
167 |
|
168 if (this.app.updateManifest) { |
|
169 this.webappJson.app.updateManifest = this.app.updateManifest; |
|
170 } |
|
171 |
|
172 this.runtimeFolder = OS.Constants.Path.libDir; |
|
173 }, |
|
174 |
|
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)); |
|
187 |
|
188 let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath; |
|
189 |
|
190 this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath, |
|
191 null, null); |
|
192 } |
|
193 |
|
194 |
|
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); |
|
200 |
|
201 let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null); |
|
202 |
|
203 let [ mimeType, icon ] = yield downloadIcon(iconURI); |
|
204 yield this._processIcon(mimeType, icon, aTmpDir); |
|
205 |
|
206 // Set the iconURI property so that the user notification will have the |
|
207 // correct icon. |
|
208 this.iconURI = iconURI; |
|
209 } |
|
210 }, |
|
211 |
|
212 /** |
|
213 * Creates the profile to be used for this app. |
|
214 */ |
|
215 createProfile: function() { |
|
216 if (this._dryRun) { |
|
217 return null; |
|
218 } |
|
219 |
|
220 let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]. |
|
221 getService(Ci.nsIToolkitProfileService); |
|
222 |
|
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 }; |
|
232 |
|
233 #ifdef XP_WIN |
|
234 |
|
235 #include WinNativeApp.js |
|
236 |
|
237 #elifdef XP_MACOSX |
|
238 |
|
239 #include MacNativeApp.js |
|
240 |
|
241 #elifdef XP_UNIX |
|
242 |
|
243 #include LinuxNativeApp.js |
|
244 |
|
245 #endif |
|
246 |
|
247 /* Helper Functions */ |
|
248 |
|
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); |
|
258 |
|
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 } |
|
269 |
|
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 } |
|
277 |
|
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"); |
|
286 |
|
287 // Strip white space characters starting from the last [0-9a-zA-Z] |
|
288 let stripBackRE = new RegExp("\\s*$", "gi"); |
|
289 |
|
290 // Strip invalid characters from the filename |
|
291 let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); |
|
292 |
|
293 let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, ""); |
|
294 stripped = stripped.replace(stripBackRE, ""); |
|
295 stripped = stripped.replace(filenameRE, ""); |
|
296 |
|
297 // If the filename ends up empty, let's call it "webapp". |
|
298 if (stripped == "") { |
|
299 stripped = "webapp"; |
|
300 } |
|
301 |
|
302 return stripped; |
|
303 } |
|
304 |
|
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 |
|
312 |
|
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; |
|
319 |
|
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 } |
|
327 |
|
328 return true; |
|
329 }); |
|
330 } |
|
331 |
|
332 if (yield checkUnique(name)) { |
|
333 return name; |
|
334 } |
|
335 |
|
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; |
|
340 |
|
341 if (yield checkUnique(name)) { |
|
342 return name; |
|
343 } |
|
344 } |
|
345 |
|
346 throw "No available filename"; |
|
347 }); |
|
348 } |
|
349 |
|
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); |
|
358 |
|
359 try { |
|
360 if (file.exists()) { |
|
361 file.followLinks = false; |
|
362 file.remove(true); |
|
363 } |
|
364 } catch(ex) {} |
|
365 } |
|
366 } |
|
367 |
|
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); |
|
377 |
|
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 } |
|
388 |
|
389 // The source directory is now empty, remove it. |
|
390 yield OS.File.removeEmptyDir(srcPath); |
|
391 } |
|
392 |
|
393 function escapeXML(aStr) { |
|
394 return aStr.toString() |
|
395 .replace(/&/g, "&") |
|
396 .replace(/"/g, """) |
|
397 .replace(/'/g, "'") |
|
398 .replace(/</g, "<") |
|
399 .replace(/>/g, ">"); |
|
400 } |
|
401 |
|
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 } |
|
408 |
|
409 /* More helpers for handling the app icon */ |
|
410 #include WebappsIconHelpers.js |