|
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 } |