toolkit/webapps/WebappOSUtils.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:9d95d22e666e
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 }

mercurial