toolkit/webapps/WinNativeApp.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:7c5c82155ef2
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 const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir;
6 const APP_DATA_DIR = OS.Constants.Path.winAppDataDir;
7
8 /*************************************
9 * Windows app installer
10 *
11 * The Windows installation process will generate the following files:
12 *
13 * ${FolderName} = sanitized app name + "-" + manifest url hash
14 *
15 * %APPDATA%/${FolderName}
16 * - webapp.ini
17 * - webapp.json
18 * - ${AppName}.exe
19 * - ${AppName}.lnk
20 * / uninstall
21 * - webapp-uninstaller.exe
22 * - shortcuts_log.ini
23 * - uninstall.log
24 * / chrome/icons/default/
25 * - default.ico
26 *
27 * After the app runs for the first time, a profiles/ folder will also be
28 * created which will host the user profile for this app.
29 */
30
31 /**
32 * Constructor for the Windows native app shell
33 *
34 * @param aApp {Object} the app object provided to the install function
35 * @param aManifest {Object} the manifest data provided by the web app
36 * @param aCategories {Array} array of app categories
37 * @param aRegistryDir {String} (optional) path to the registry
38 */
39 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
40 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
41
42 if (this.isPackaged) {
43 this.size = aApp.updateManifest.size / 1024;
44 }
45
46 this.webapprt = this.appNameAsFilename + ".exe";
47 this.configJson = "webapp.json";
48 this.webappINI = "webapp.ini";
49 this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico");
50 this.uninstallDir = "uninstall";
51 this.uninstallerFile = OS.Path.join(this.uninstallDir,
52 "webapp-uninstaller.exe");
53 this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini");
54 this.zipFile = "application.zip";
55
56 this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ];
57 if (this.isPackaged) {
58 this.backupFiles.push(this.zipFile);
59 }
60
61 this.uninstallSubkeyStr = this.uniqueName;
62 }
63
64 NativeApp.prototype = {
65 __proto__: CommonNativeApp.prototype,
66 size: null,
67
68 /**
69 * Creates a native installation of the web app in the OS
70 *
71 * @param aManifest {Object} the manifest data provided by the web app
72 * @param aZipPath {String} path to the zip file for packaged apps (undefined
73 * for hosted apps)
74 */
75 install: Task.async(function*(aManifest, aZipPath) {
76 if (this._dryRun) {
77 return;
78 }
79
80 // If the application is already installed, this is a reinstallation.
81 if (WebappOSUtils.getInstallPath(this.app)) {
82 return yield this.prepareUpdate(aManifest, aZipPath);
83 }
84
85 this._setData(aManifest);
86
87 let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName);
88
89 // Create a temporary installation directory.
90 let dir = getFile(TMP_DIR, this.uniqueName);
91 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
92 let tmpDir = dir.path;
93
94 // Perform the installation in the temp directory.
95 try {
96 yield this._createDirectoryStructure(tmpDir);
97 yield this._getShortcutName(installDir);
98 yield this._copyWebapprt(tmpDir);
99 yield this._copyUninstaller(tmpDir);
100 yield this._createConfigFiles(tmpDir);
101
102 if (aZipPath) {
103 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
104 }
105
106 yield this._getIcon(tmpDir);
107 } catch (ex) {
108 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
109 throw ex;
110 }
111
112 // Apply the installation.
113 this._removeInstallation(true, installDir);
114
115 try {
116 yield this._applyTempInstallation(tmpDir, installDir);
117 } catch (ex) {
118 this._removeInstallation(false, installDir);
119 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
120 throw ex;
121 }
122 }),
123
124 /**
125 * Creates an update in a temporary directory to be applied later.
126 *
127 * @param aManifest {Object} the manifest data provided by the web app
128 * @param aZipPath {String} path to the zip file for packaged apps (undefined
129 * for hosted apps)
130 */
131 prepareUpdate: Task.async(function*(aManifest, aZipPath) {
132 if (this._dryRun) {
133 return;
134 }
135
136 this._setData(aManifest);
137
138 let installDir = WebappOSUtils.getInstallPath(this.app);
139 if (!installDir) {
140 throw ERR_NOT_INSTALLED;
141 }
142
143 if (this.uniqueName != OS.Path.basename(installDir)) {
144 // Bug 919799: If the app is still in the registry, migrate its data to
145 // the new format.
146 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
147 }
148
149 let updateDir = OS.Path.join(installDir, "update");
150 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
151 yield OS.File.makeDir(updateDir);
152
153 // Perform the update in the "update" subdirectory.
154 try {
155 yield this._createDirectoryStructure(updateDir);
156 yield this._getShortcutName(installDir);
157 yield this._copyUninstaller(updateDir);
158 yield this._createConfigFiles(updateDir);
159
160 if (aZipPath) {
161 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
162 }
163
164 yield this._getIcon(updateDir);
165 } catch (ex) {
166 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
167 throw ex;
168 }
169 }),
170
171 /**
172 * Applies an update.
173 */
174 applyUpdate: Task.async(function*() {
175 if (this._dryRun) {
176 return;
177 }
178
179 let installDir = WebappOSUtils.getInstallPath(this.app);
180 let updateDir = OS.Path.join(installDir, "update");
181
182 yield this._getShortcutName(installDir);
183
184 let backupDir = yield this._backupInstallation(installDir);
185
186 try {
187 yield this._applyTempInstallation(updateDir, installDir);
188 } catch (ex) {
189 yield this._restoreInstallation(backupDir, installDir);
190 throw ex;
191 } finally {
192 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
193 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
194 }
195 }),
196
197 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
198 yield moveDirectory(aTmpDir, aInstallDir);
199
200 this._createShortcutFiles(aInstallDir);
201 this._writeSystemKeys(aInstallDir);
202 }),
203
204 _getShortcutName: Task.async(function*(aInstallDir) {
205 let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI);
206
207 if (shortcutLogsINIfile.exists()) {
208 // If it's a reinstallation (or an update) get the shortcut names
209 // from the shortcut_log.ini file
210 let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
211 getService(Ci.nsIINIParserFactory).
212 createINIParser(shortcutLogsINIfile);
213 this.shortcutName = parser.getString("STARTMENU", "Shortcut0");
214 } else {
215 // Check in both directories to see if a shortcut with the same name
216 // already exists.
217 this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
218 this.appNameAsFilename,
219 ".lnk");
220 }
221 }),
222
223 /**
224 * Remove the current installation
225 */
226 _removeInstallation: function(keepProfile, aInstallDir) {
227 let uninstallKey;
228 try {
229 uninstallKey = Cc["@mozilla.org/windows-registry-key;1"].
230 createInstance(Ci.nsIWindowsRegKey);
231 uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
232 "SOFTWARE\\Microsoft\\Windows\\" +
233 "CurrentVersion\\Uninstall",
234 uninstallKey.ACCESS_WRITE);
235 if (uninstallKey.hasChild(this.uninstallSubkeyStr)) {
236 uninstallKey.removeChild(this.uninstallSubkeyStr);
237 }
238 } catch (e) {
239 } finally {
240 if (uninstallKey) {
241 uninstallKey.close();
242 }
243 }
244
245 let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName),
246 OS.Path.join(PROGS_DIR, this.shortcutName) ];
247
248 if (keepProfile) {
249 for (let filePath of this.backupFiles) {
250 filesToRemove.push(OS.Path.join(aInstallDir, filePath));
251 }
252
253 filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
254 } else {
255 filesToRemove.push(aInstallDir);
256 }
257
258 removeFiles(filesToRemove);
259 },
260
261 _backupInstallation: Task.async(function*(aInstallDir) {
262 let backupDir = OS.Path.join(aInstallDir, "backup");
263 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
264 yield OS.File.makeDir(backupDir);
265
266 for (let filePath of this.backupFiles) {
267 yield OS.File.move(OS.Path.join(aInstallDir, filePath),
268 OS.Path.join(backupDir, filePath));
269 }
270
271 return backupDir;
272 }),
273
274 _restoreInstallation: function(aBackupDir, aInstallDir) {
275 return moveDirectory(aBackupDir, aInstallDir);
276 },
277
278 /**
279 * Creates the main directory structure.
280 */
281 _createDirectoryStructure: Task.async(function*(aDir) {
282 yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir));
283
284 yield OS.File.makeDir(OS.Path.join(aDir, OS.Path.dirname(this.iconPath)),
285 { from: aDir });
286 }),
287
288 /**
289 * Copy the webrt executable into the installation directory.
290 */
291 _copyWebapprt: function(aDir) {
292 return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"),
293 OS.Path.join(aDir, this.webapprt));
294 },
295
296 /**
297 * Copy the uninstaller executable into the installation directory.
298 */
299 _copyUninstaller: function(aDir) {
300 return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"),
301 OS.Path.join(aDir, this.uninstallerFile));
302 },
303
304 /**
305 * Creates the configuration files into their destination folders.
306 */
307 _createConfigFiles: function(aDir) {
308 // ${InstallDir}/webapp.json
309 yield writeToFile(OS.Path.join(aDir, this.configJson),
310 JSON.stringify(this.webappJson));
311
312 let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
313 getService(Ci.nsIINIParserFactory);
314
315 // ${InstallDir}/webapp.ini
316 let webappINIfile = getFile(aDir, this.webappINI);
317
318 let writer = factory.createINIParser(webappINIfile)
319 .QueryInterface(Ci.nsIINIParserWriter);
320 writer.setString("Webapp", "Name", this.appName);
321 writer.setString("Webapp", "Profile", this.uniqueName);
322 writer.setString("Webapp", "Executable", this.appNameAsFilename);
323 writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
324 writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
325
326 let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI);
327
328 writer = factory.createINIParser(shortcutLogsINIfile)
329 .QueryInterface(Ci.nsIINIParserWriter);
330 writer.setString("STARTMENU", "Shortcut0", this.shortcutName);
331 writer.setString("DESKTOP", "Shortcut0", this.shortcutName);
332 writer.setString("TASKBAR", "Migrated", "true");
333 writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
334
335 // ${UninstallDir}/uninstall.log
336 let uninstallContent =
337 "File: \\webapp.ini\r\n" +
338 "File: \\webapp.json\r\n" +
339 "File: \\webapprt.old\r\n" +
340 "File: \\chrome\\icons\\default\\default.ico";
341 if (this.isPackaged) {
342 uninstallContent += "\r\nFile: \\application.zip";
343 }
344
345 yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"),
346 uninstallContent);
347 },
348
349 /**
350 * Writes the keys to the system registry that are necessary for the app
351 * operation and uninstall process.
352 */
353 _writeSystemKeys: function(aInstallDir) {
354 let parentKey;
355 let uninstallKey;
356 let subKey;
357
358 try {
359 parentKey = Cc["@mozilla.org/windows-registry-key;1"].
360 createInstance(Ci.nsIWindowsRegKey);
361 parentKey.open(parentKey.ROOT_KEY_CURRENT_USER,
362 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
363 parentKey.ACCESS_WRITE);
364 uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE)
365 subKey = uninstallKey.createChild(this.uninstallSubkeyStr,
366 uninstallKey.ACCESS_WRITE);
367
368 subKey.writeStringValue("DisplayName", this.appName);
369
370 let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile);
371
372 subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"');
373 subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"');
374 subKey.writeStringValue("AppFilename", this.appNameAsFilename);
375 subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir,
376 this.iconPath));
377
378 let date = new Date();
379 let year = date.getYear().toString();
380 let month = date.getMonth();
381 if (month < 10) {
382 month = "0" + month;
383 }
384 let day = date.getDate();
385 if (day < 10) {
386 day = "0" + day;
387 }
388 subKey.writeStringValue("InstallDate", year + month + day);
389 if (this.version) {
390 subKey.writeStringValue("DisplayVersion", this.version);
391 }
392 if (this.developerName) {
393 subKey.writeStringValue("Publisher", this.developerName);
394 }
395 subKey.writeStringValue("URLInfoAbout", this.developerUrl);
396 if (this.size) {
397 subKey.writeIntValue("EstimatedSize", this.size);
398 }
399
400 subKey.writeIntValue("NoModify", 1);
401 subKey.writeIntValue("NoRepair", 1);
402 } catch(ex) {
403 throw ex;
404 } finally {
405 if(subKey) subKey.close();
406 if(uninstallKey) uninstallKey.close();
407 if(parentKey) parentKey.close();
408 }
409 },
410
411 /**
412 * Creates a shortcut file inside the app installation folder and makes
413 * two copies of it: one into the desktop and one into the start menu.
414 */
415 _createShortcutFiles: function(aInstallDir) {
416 let shortcut = getFile(aInstallDir, this.shortcutName).
417 QueryInterface(Ci.nsILocalFileWin);
418
419 /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args,
420 description, iconFile, iconIndex) */
421
422 shortcut.setShortcut(getFile(aInstallDir, this.webapprt),
423 getFile(aInstallDir),
424 null,
425 this.shortDescription,
426 getFile(aInstallDir, this.iconPath),
427 0);
428
429 shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName);
430 shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName);
431
432 shortcut.followLinks = false;
433 shortcut.remove(false);
434 },
435
436 /**
437 * Process the icon from the imageStream as retrieved from
438 * the URL by getIconForApp(). This will save the icon to the
439 * topwindow.ico file.
440 *
441 * @param aMimeType the icon mimetype
442 * @param aImageStream the stream for the image data
443 * @param aDir the directory where the icon should be stored
444 */
445 _processIcon: function(aMimeType, aImageStream, aDir) {
446 let deferred = Promise.defer();
447
448 let imgTools = Cc["@mozilla.org/image/tools;1"].
449 createInstance(Ci.imgITools);
450
451 let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
452 let iconStream = imgTools.encodeImage(imgContainer,
453 "image/vnd.microsoft.icon",
454 "format=bmp;bpp=32");
455
456 let tmpIconFile = getFile(aDir, this.iconPath);
457
458 let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile);
459 NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
460 if (Components.isSuccessCode(aResult)) {
461 deferred.resolve();
462 } else {
463 deferred.reject("Failure copying icon: " + aResult);
464 }
465 });
466
467 return deferred.promise;
468 }
469 }

mercurial