|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components; |
|
6 |
|
7 Cu.import("resource://gre/modules/Services.jsm"); |
|
8 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
9 Cu.import("resource://gre/modules/osfile.jsm"); |
|
10 Cu.import("resource://gre/modules/Promise.jsm"); |
|
11 |
|
12 this.EXPORTED_SYMBOLS = ["WebappOSUtils"]; |
|
13 |
|
14 // Returns the MD5 hash of a string. |
|
15 function computeHash(aString) { |
|
16 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
|
17 createInstance(Ci.nsIScriptableUnicodeConverter); |
|
18 converter.charset = "UTF-8"; |
|
19 let result = {}; |
|
20 // Data is an array of bytes. |
|
21 let data = converter.convertToByteArray(aString, result); |
|
22 |
|
23 let hasher = Cc["@mozilla.org/security/hash;1"]. |
|
24 createInstance(Ci.nsICryptoHash); |
|
25 hasher.init(hasher.MD5); |
|
26 hasher.update(data, data.length); |
|
27 // We're passing false to get the binary hash and not base64. |
|
28 let hash = hasher.finish(false); |
|
29 |
|
30 function toHexString(charCode) { |
|
31 return ("0" + charCode.toString(16)).slice(-2); |
|
32 } |
|
33 |
|
34 // Convert the binary hash data to a hex string. |
|
35 return [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); |
|
36 } |
|
37 |
|
38 this.WebappOSUtils = { |
|
39 getUniqueName: function(aApp) { |
|
40 return this.sanitizeStringForFilename(aApp.name).toLowerCase() + "-" + |
|
41 computeHash(aApp.manifestURL); |
|
42 }, |
|
43 |
|
44 #ifdef XP_WIN |
|
45 /** |
|
46 * Returns the registry key associated to the given app and a boolean that |
|
47 * specifies whether we're using the old naming scheme or the new one. |
|
48 */ |
|
49 getAppRegKey: function(aApp) { |
|
50 let regKey = Cc["@mozilla.org/windows-registry-key;1"]. |
|
51 createInstance(Ci.nsIWindowsRegKey); |
|
52 |
|
53 try { |
|
54 regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, |
|
55 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + |
|
56 this.getUniqueName(aApp), Ci.nsIWindowsRegKey.ACCESS_READ); |
|
57 |
|
58 return { value: regKey, |
|
59 namingSchemeVersion: 2}; |
|
60 } catch (ex) {} |
|
61 |
|
62 // Fall back to the old installation naming scheme |
|
63 try { |
|
64 regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, |
|
65 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + |
|
66 aApp.origin, Ci.nsIWindowsRegKey.ACCESS_READ); |
|
67 |
|
68 return { value: regKey, |
|
69 namingSchemeVersion: 1 }; |
|
70 } catch (ex) {} |
|
71 |
|
72 return null; |
|
73 }, |
|
74 #endif |
|
75 |
|
76 /** |
|
77 * Returns the executable of the given app, identifying it by its unique name, |
|
78 * which is in either the new format or the old format. |
|
79 * On Mac OS X, it returns the identifier of the app. |
|
80 * |
|
81 * The new format ensures a readable and unique name for an app by combining |
|
82 * its name with a hash of its manifest URL. The old format uses its origin, |
|
83 * which is only unique until we support multiple apps per origin. |
|
84 */ |
|
85 getLaunchTarget: function(aApp) { |
|
86 #ifdef XP_WIN |
|
87 let appRegKey = this.getAppRegKey(aApp); |
|
88 |
|
89 if (!appRegKey) { |
|
90 return null; |
|
91 } |
|
92 |
|
93 let appFilename, installLocation; |
|
94 try { |
|
95 appFilename = appRegKey.value.readStringValue("AppFilename"); |
|
96 installLocation = appRegKey.value.readStringValue("InstallLocation"); |
|
97 } catch (ex) { |
|
98 return null; |
|
99 } finally { |
|
100 appRegKey.value.close(); |
|
101 } |
|
102 |
|
103 installLocation = installLocation.substring(1, installLocation.length - 1); |
|
104 |
|
105 if (appRegKey.namingSchemeVersion == 1 && |
|
106 !this.isOldInstallPathValid(aApp, installLocation)) { |
|
107 return null; |
|
108 } |
|
109 |
|
110 let initWithPath = CC("@mozilla.org/file/local;1", |
|
111 "nsILocalFile", "initWithPath"); |
|
112 let launchTarget = initWithPath(installLocation); |
|
113 launchTarget.append(appFilename + ".exe"); |
|
114 |
|
115 return launchTarget; |
|
116 #elifdef XP_MACOSX |
|
117 let uniqueName = this.getUniqueName(aApp); |
|
118 |
|
119 let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. |
|
120 createInstance(Ci.nsIMacWebAppUtils); |
|
121 |
|
122 try { |
|
123 let path; |
|
124 if (path = mwaUtils.pathForAppWithIdentifier(uniqueName)) { |
|
125 return [ uniqueName, path ]; |
|
126 } |
|
127 } catch(ex) {} |
|
128 |
|
129 // Fall back to the old installation naming scheme |
|
130 try { |
|
131 let path; |
|
132 if ((path = mwaUtils.pathForAppWithIdentifier(aApp.origin)) && |
|
133 this.isOldInstallPathValid(aApp, path)) { |
|
134 return [ aApp.origin, path ]; |
|
135 } |
|
136 } catch(ex) {} |
|
137 |
|
138 return [ null, null ]; |
|
139 #elifdef XP_UNIX |
|
140 let uniqueName = this.getUniqueName(aApp); |
|
141 |
|
142 let exeFile = Services.dirsvc.get("Home", Ci.nsIFile); |
|
143 exeFile.append("." + uniqueName); |
|
144 exeFile.append("webapprt-stub"); |
|
145 |
|
146 // Fall back to the old installation naming scheme |
|
147 if (!exeFile.exists()) { |
|
148 exeFile = Services.dirsvc.get("Home", Ci.nsIFile); |
|
149 |
|
150 let origin = Services.io.newURI(aApp.origin, null, null); |
|
151 let installDir = "." + origin.scheme + ";" + |
|
152 origin.host + |
|
153 (origin.port != -1 ? ";" + origin.port : ""); |
|
154 |
|
155 exeFile.append(installDir); |
|
156 exeFile.append("webapprt-stub"); |
|
157 |
|
158 if (!exeFile.exists() || |
|
159 !this.isOldInstallPathValid(aApp, exeFile.parent.path)) { |
|
160 return null; |
|
161 } |
|
162 } |
|
163 |
|
164 return exeFile; |
|
165 #endif |
|
166 }, |
|
167 |
|
168 getInstallPath: function(aApp) { |
|
169 #ifdef MOZ_B2G |
|
170 // All b2g builds |
|
171 return aApp.basePath + "/" + aApp.id; |
|
172 |
|
173 #elifdef MOZ_FENNEC |
|
174 // All fennec |
|
175 return aApp.basePath + "/" + aApp.id; |
|
176 |
|
177 #elifdef MOZ_PHOENIX |
|
178 // Firefox |
|
179 |
|
180 #ifdef XP_WIN |
|
181 let execFile = this.getLaunchTarget(aApp); |
|
182 if (!execFile) { |
|
183 return null; |
|
184 } |
|
185 |
|
186 return execFile.parent.path; |
|
187 #elifdef XP_MACOSX |
|
188 let [ bundleID, path ] = this.getLaunchTarget(aApp); |
|
189 return path; |
|
190 #elifdef XP_UNIX |
|
191 let execFile = this.getLaunchTarget(aApp); |
|
192 if (!execFile) { |
|
193 return null; |
|
194 } |
|
195 |
|
196 return execFile.parent.path; |
|
197 #endif |
|
198 |
|
199 #elifdef MOZ_WEBAPP_RUNTIME |
|
200 // Webapp runtime |
|
201 |
|
202 #ifdef XP_WIN |
|
203 let execFile = this.getLaunchTarget(aApp); |
|
204 if (!execFile) { |
|
205 return null; |
|
206 } |
|
207 |
|
208 return execFile.parent.path; |
|
209 #elifdef XP_MACOSX |
|
210 let [ bundleID, path ] = this.getLaunchTarget(aApp); |
|
211 return path; |
|
212 #elifdef XP_UNIX |
|
213 let execFile = this.getLaunchTarget(aApp); |
|
214 if (!execFile) { |
|
215 return null; |
|
216 } |
|
217 |
|
218 return execFile.parent.path; |
|
219 #endif |
|
220 |
|
221 #endif |
|
222 // Anything unsupported, like Metro |
|
223 throw new Error("Unsupported apps platform"); |
|
224 }, |
|
225 |
|
226 getPackagePath: function(aApp) { |
|
227 let packagePath = this.getInstallPath(aApp); |
|
228 |
|
229 // Only for Firefox on Mac OS X |
|
230 #ifndef MOZ_B2G |
|
231 #ifdef XP_MACOSX |
|
232 packagePath = OS.Path.join(packagePath, "Contents", "Resources"); |
|
233 #endif |
|
234 #endif |
|
235 |
|
236 return packagePath; |
|
237 }, |
|
238 |
|
239 launch: function(aApp) { |
|
240 let uniqueName = this.getUniqueName(aApp); |
|
241 |
|
242 #ifdef XP_WIN |
|
243 let launchTarget = this.getLaunchTarget(aApp); |
|
244 if (!launchTarget) { |
|
245 return false; |
|
246 } |
|
247 |
|
248 try { |
|
249 let process = Cc["@mozilla.org/process/util;1"]. |
|
250 createInstance(Ci.nsIProcess); |
|
251 |
|
252 process.init(launchTarget); |
|
253 process.runwAsync([], 0); |
|
254 } catch (e) { |
|
255 return false; |
|
256 } |
|
257 |
|
258 return true; |
|
259 #elifdef XP_MACOSX |
|
260 let [ launchIdentifier, path ] = this.getLaunchTarget(aApp); |
|
261 if (!launchIdentifier) { |
|
262 return false; |
|
263 } |
|
264 |
|
265 let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. |
|
266 createInstance(Ci.nsIMacWebAppUtils); |
|
267 |
|
268 try { |
|
269 mwaUtils.launchAppWithIdentifier(launchIdentifier); |
|
270 } catch(e) { |
|
271 return false; |
|
272 } |
|
273 |
|
274 return true; |
|
275 #elifdef XP_UNIX |
|
276 let exeFile = this.getLaunchTarget(aApp); |
|
277 if (!exeFile) { |
|
278 return false; |
|
279 } |
|
280 |
|
281 try { |
|
282 let process = Cc["@mozilla.org/process/util;1"] |
|
283 .createInstance(Ci.nsIProcess); |
|
284 |
|
285 process.init(exeFile); |
|
286 process.runAsync([], 0); |
|
287 } catch (e) { |
|
288 return false; |
|
289 } |
|
290 |
|
291 return true; |
|
292 #endif |
|
293 }, |
|
294 |
|
295 uninstall: function(aApp) { |
|
296 #ifdef XP_WIN |
|
297 let appRegKey = this.getAppRegKey(aApp); |
|
298 |
|
299 if (!appRegKey) { |
|
300 return Promise.reject("App registry key not found"); |
|
301 } |
|
302 |
|
303 let deferred = Promise.defer(); |
|
304 |
|
305 try { |
|
306 let uninstallerPath = appRegKey.value.readStringValue("UninstallString"); |
|
307 uninstallerPath = uninstallerPath.substring(1, uninstallerPath.length - 1); |
|
308 |
|
309 let uninstaller = Cc["@mozilla.org/file/local;1"]. |
|
310 createInstance(Ci.nsIFile); |
|
311 uninstaller.initWithPath(uninstallerPath); |
|
312 |
|
313 let process = Cc["@mozilla.org/process/util;1"]. |
|
314 createInstance(Ci.nsIProcess); |
|
315 process.init(uninstaller); |
|
316 process.runwAsync(["/S"], 1, (aSubject, aTopic) => { |
|
317 if (aTopic == "process-finished") { |
|
318 deferred.resolve(true); |
|
319 } else { |
|
320 deferred.reject("Uninstaller failed with exit code: " + aSubject.exitValue); |
|
321 } |
|
322 }); |
|
323 } catch (e) { |
|
324 deferred.reject(e); |
|
325 } finally { |
|
326 appRegKey.value.close(); |
|
327 } |
|
328 |
|
329 return deferred.promise; |
|
330 #elifdef XP_MACOSX |
|
331 let [ , path ] = this.getLaunchTarget(aApp); |
|
332 if (!path) { |
|
333 return Promise.reject("App not found"); |
|
334 } |
|
335 |
|
336 let deferred = Promise.defer(); |
|
337 |
|
338 let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. |
|
339 createInstance(Ci.nsIMacWebAppUtils); |
|
340 |
|
341 mwaUtils.trashApp(path, (aResult) => { |
|
342 if (aResult == Cr.NS_OK) { |
|
343 deferred.resolve(true); |
|
344 } else { |
|
345 deferred.resolve("Error moving the app to the Trash: " + aResult); |
|
346 } |
|
347 }); |
|
348 |
|
349 return deferred.promise; |
|
350 #elifdef XP_UNIX |
|
351 let exeFile = this.getLaunchTarget(aApp); |
|
352 if (!exeFile) { |
|
353 return Promise.reject("App executable file not found"); |
|
354 } |
|
355 |
|
356 let deferred = Promise.defer(); |
|
357 |
|
358 try { |
|
359 let process = Cc["@mozilla.org/process/util;1"] |
|
360 .createInstance(Ci.nsIProcess); |
|
361 |
|
362 process.init(exeFile); |
|
363 process.runAsync(["-remove"], 1, (aSubject, aTopic) => { |
|
364 if (aTopic == "process-finished") { |
|
365 deferred.resolve(true); |
|
366 } else { |
|
367 deferred.reject("Uninstaller failed with exit code: " + aSubject.exitValue); |
|
368 } |
|
369 }); |
|
370 } catch (e) { |
|
371 deferred.reject(e); |
|
372 } |
|
373 |
|
374 return deferred.promise; |
|
375 #endif |
|
376 }, |
|
377 |
|
378 /** |
|
379 * Returns true if the given install path (in the old naming scheme) actually |
|
380 * belongs to the given application. |
|
381 */ |
|
382 isOldInstallPathValid: function(aApp, aInstallPath) { |
|
383 // Applications with an origin that starts with "app" are packaged apps and |
|
384 // packaged apps have never been installed using the old naming scheme. |
|
385 // After bug 910465, we'll have a better way to check if an app is |
|
386 // packaged. |
|
387 if (aApp.origin.startsWith("app")) { |
|
388 return false; |
|
389 } |
|
390 |
|
391 // Bug 915480: We could check the app name from the manifest to |
|
392 // better verify the installation path. |
|
393 return true; |
|
394 }, |
|
395 |
|
396 /** |
|
397 * Checks if the given app is locally installed. |
|
398 */ |
|
399 isLaunchable: function(aApp) { |
|
400 let uniqueName = this.getUniqueName(aApp); |
|
401 |
|
402 #ifdef XP_WIN |
|
403 if (!this.getLaunchTarget(aApp)) { |
|
404 return false; |
|
405 } |
|
406 |
|
407 return true; |
|
408 #elifdef XP_MACOSX |
|
409 if (!this.getInstallPath(aApp)) { |
|
410 return false; |
|
411 } |
|
412 |
|
413 return true; |
|
414 #elifdef XP_UNIX |
|
415 let env = Cc["@mozilla.org/process/environment;1"] |
|
416 .getService(Ci.nsIEnvironment); |
|
417 |
|
418 let xdg_data_home_env; |
|
419 try { |
|
420 xdg_data_home_env = env.get("XDG_DATA_HOME"); |
|
421 } catch(ex) {} |
|
422 |
|
423 let desktopINI; |
|
424 if (xdg_data_home_env) { |
|
425 desktopINI = new FileUtils.File(xdg_data_home_env); |
|
426 } else { |
|
427 desktopINI = FileUtils.getFile("Home", [".local", "share"]); |
|
428 } |
|
429 desktopINI.append("applications"); |
|
430 desktopINI.append("owa-" + uniqueName + ".desktop"); |
|
431 |
|
432 // Fall back to the old installation naming scheme |
|
433 if (!desktopINI.exists()) { |
|
434 if (xdg_data_home_env) { |
|
435 desktopINI = new FileUtils.File(xdg_data_home_env); |
|
436 } else { |
|
437 desktopINI = FileUtils.getFile("Home", [".local", "share"]); |
|
438 } |
|
439 |
|
440 let origin = Services.io.newURI(aApp.origin, null, null); |
|
441 let oldUniqueName = origin.scheme + ";" + |
|
442 origin.host + |
|
443 (origin.port != -1 ? ";" + origin.port : ""); |
|
444 |
|
445 desktopINI.append("owa-" + oldUniqueName + ".desktop"); |
|
446 |
|
447 if (!desktopINI.exists()) { |
|
448 return false; |
|
449 } |
|
450 |
|
451 let installDir = Services.dirsvc.get("Home", Ci.nsIFile); |
|
452 installDir.append("." + origin.scheme + ";" + origin.host + |
|
453 (origin.port != -1 ? ";" + origin.port : "")); |
|
454 |
|
455 return isOldInstallPathValid(aApp, installDir.path); |
|
456 } |
|
457 |
|
458 return true; |
|
459 #endif |
|
460 }, |
|
461 |
|
462 /** |
|
463 * Sanitize the filename (accepts only a-z, 0-9, - and _) |
|
464 */ |
|
465 sanitizeStringForFilename: function(aPossiblyBadFilenameString) { |
|
466 return aPossiblyBadFilenameString.replace(/[^a-z0-9_\-]/gi, ""); |
|
467 } |
|
468 } |