toolkit/webapps/MacNativeApp.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:335882ea6cd3
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 USER_LIB_DIR = OS.Constants.Path.macUserLibDir;
6 const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir;
7
8 /**
9 * Constructor for the Mac native app shell
10 *
11 * @param aApp {Object} the app object provided to the install function
12 * @param aManifest {Object} the manifest data provided by the web app
13 * @param aCategories {Array} array of app categories
14 * @param aRegistryDir {String} (optional) path to the registry
15 */
16 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
17 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
18
19 // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash
20 this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support",
21 this.uniqueName);
22 this.configJson = "webapp.json";
23
24 this.contentsDir = "Contents";
25 this.macOSDir = OS.Path.join(this.contentsDir, "MacOS");
26 this.resourcesDir = OS.Path.join(this.contentsDir, "Resources");
27 this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns");
28 this.zipFile = OS.Path.join(this.resourcesDir, "application.zip");
29 }
30
31 NativeApp.prototype = {
32 __proto__: CommonNativeApp.prototype,
33 /*
34 * The _rootInstallDir property is the path of the directory where we install
35 * apps. In production code, it's "/Applications". In tests, it's
36 * "~/Applications" because on build machines we don't have enough privileges
37 * to write to the global "/Applications" directory.
38 */
39 _rootInstallDir: LOCAL_APP_DIR,
40
41 /**
42 * Creates a native installation of the web app in the OS
43 *
44 * @param aManifest {Object} the manifest data provided by the web app
45 * @param aZipPath {String} path to the zip file for packaged apps (undefined
46 * for hosted apps)
47 */
48 install: Task.async(function*(aManifest, aZipPath) {
49 if (this._dryRun) {
50 return;
51 }
52
53 // If the application is already installed, this is a reinstallation.
54 if (WebappOSUtils.getInstallPath(this.app)) {
55 return yield this.prepareUpdate(aManifest, aZipPath);
56 }
57
58 this._setData(aManifest);
59
60 let localAppDir = getFile(this._rootInstallDir);
61 if (!localAppDir.isWritable()) {
62 throw("Not enough privileges to install apps");
63 }
64
65 let destinationName = yield getAvailableFileName([ this._rootInstallDir ],
66 this.appNameAsFilename,
67 ".app");
68
69 let installDir = OS.Path.join(this._rootInstallDir, destinationName);
70
71 let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app");
72 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
73 let tmpDir = dir.path;
74
75 try {
76 yield this._createDirectoryStructure(tmpDir);
77 this._copyPrebuiltFiles(tmpDir);
78 yield this._createConfigFiles(tmpDir);
79
80 if (aZipPath) {
81 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
82 }
83
84 yield this._getIcon(tmpDir);
85 } catch (ex) {
86 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
87 throw ex;
88 }
89
90 this._removeInstallation(true, installDir);
91
92 try {
93 // Move the temp installation directory to the /Applications directory
94 yield this._applyTempInstallation(tmpDir, installDir);
95 } catch (ex) {
96 this._removeInstallation(false, installDir);
97 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
98 throw ex;
99 }
100 }),
101
102 /**
103 * Creates an update in a temporary directory to be applied later.
104 *
105 * @param aManifest {Object} the manifest data provided by the web app
106 * @param aZipPath {String} path to the zip file for packaged apps (undefined
107 * for hosted apps)
108 */
109 prepareUpdate: Task.async(function*(aManifest, aZipPath) {
110 if (this._dryRun) {
111 return;
112 }
113
114 this._setData(aManifest);
115
116 let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app);
117 if (!installDir) {
118 throw ERR_NOT_INSTALLED;
119 }
120
121 if (this.uniqueName != oldUniqueName) {
122 // Bug 919799: If the app is still in the registry, migrate its data to
123 // the new format.
124 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
125 }
126
127 let updateDir = OS.Path.join(installDir, "update");
128 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
129 yield OS.File.makeDir(updateDir);
130
131 try {
132 yield this._createDirectoryStructure(updateDir);
133 this._copyPrebuiltFiles(updateDir);
134 yield this._createConfigFiles(updateDir);
135
136 if (aZipPath) {
137 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
138 }
139
140 yield this._getIcon(updateDir);
141 } catch (ex) {
142 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
143 throw ex;
144 }
145 }),
146
147 /**
148 * Applies an update.
149 */
150 applyUpdate: Task.async(function*() {
151 if (this._dryRun) {
152 return;
153 }
154
155 let installDir = WebappOSUtils.getInstallPath(this.app);
156 let updateDir = OS.Path.join(installDir, "update");
157
158 let backupDir = yield this._backupInstallation(installDir);
159
160 try {
161 // Move the update directory to the /Applications directory
162 yield this._applyTempInstallation(updateDir, installDir);
163 } catch (ex) {
164 yield this._restoreInstallation(backupDir, installDir);
165 throw ex;
166 } finally {
167 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
168 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
169 }
170 }),
171
172 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
173 yield OS.File.move(OS.Path.join(aTmpDir, this.configJson),
174 OS.Path.join(this.appProfileDir, this.configJson));
175
176 yield moveDirectory(aTmpDir, aInstallDir);
177 }),
178
179 _removeInstallation: function(keepProfile, aInstallDir) {
180 let filesToRemove = [ aInstallDir ];
181
182 if (!keepProfile) {
183 filesToRemove.push(this.appProfileDir);
184 }
185
186 removeFiles(filesToRemove);
187 },
188
189 _backupInstallation: Task.async(function*(aInstallDir) {
190 let backupDir = OS.Path.join(aInstallDir, "backup");
191 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
192 yield OS.File.makeDir(backupDir);
193
194 yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir),
195 backupDir);
196 yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson),
197 OS.Path.join(backupDir, this.configJson));
198
199 return backupDir;
200 }),
201
202 _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) {
203 yield OS.File.move(OS.Path.join(aBackupDir, this.configJson),
204 OS.Path.join(this.appProfileDir, this.configJson));
205 yield moveDirectory(aBackupDir,
206 OS.Path.join(aInstallDir, this.contentsDir));
207 }),
208
209 _createDirectoryStructure: Task.async(function*(aDir) {
210 yield OS.File.makeDir(this.appProfileDir,
211 { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
212
213 yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir),
214 { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
215
216 yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir),
217 { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
218
219 yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir),
220 { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
221 }),
222
223 _copyPrebuiltFiles: function(aDir) {
224 let destDir = getFile(aDir, this.macOSDir);
225 let stub = getFile(this.runtimeFolder, "webapprt-stub");
226 stub.copyTo(destDir, "webapprt");
227 },
228
229 _createConfigFiles: function(aDir) {
230 // ${ProfileDir}/webapp.json
231 yield writeToFile(OS.Path.join(aDir, this.configJson),
232 JSON.stringify(this.webappJson));
233
234 // ${InstallDir}/Contents/MacOS/webapp.ini
235 let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini");
236
237 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
238 getService(Ci.nsIINIParserFactory).
239 createINIParser(applicationINI).
240 QueryInterface(Ci.nsIINIParserWriter);
241 writer.setString("Webapp", "Name", this.appName);
242 writer.setString("Webapp", "Profile", this.uniqueName);
243 writer.writeFile();
244 applicationINI.permissions = PERMS_FILE;
245
246 // ${InstallDir}/Contents/Info.plist
247 let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\
248 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\
249 <plist version="1.0">\n\
250 <dict>\n\
251 <key>CFBundleDevelopmentRegion</key>\n\
252 <string>English</string>\n\
253 <key>CFBundleDisplayName</key>\n\
254 <string>' + escapeXML(this.appName) + '</string>\n\
255 <key>CFBundleExecutable</key>\n\
256 <string>webapprt</string>\n\
257 <key>CFBundleIconFile</key>\n\
258 <string>appicon</string>\n\
259 <key>CFBundleIdentifier</key>\n\
260 <string>' + escapeXML(this.uniqueName) + '</string>\n\
261 <key>CFBundleInfoDictionaryVersion</key>\n\
262 <string>6.0</string>\n\
263 <key>CFBundleName</key>\n\
264 <string>' + escapeXML(this.appName) + '</string>\n\
265 <key>CFBundlePackageType</key>\n\
266 <string>APPL</string>\n\
267 <key>CFBundleVersion</key>\n\
268 <string>0</string>\n\
269 <key>NSHighResolutionCapable</key>\n\
270 <true/>\n\
271 <key>NSPrincipalClass</key>\n\
272 <string>GeckoNSApplication</string>\n\
273 <key>FirefoxBinary</key>\n\
274 #expand <string>__MOZ_MACBUNDLE_ID__</string>\n\
275 </dict>\n\
276 </plist>';
277
278 yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"),
279 infoPListContent);
280 },
281
282 /**
283 * Process the icon from the imageStream as retrieved from
284 * the URL by getIconForApp(). This will bundle the icon to the
285 * app package at Contents/Resources/appicon.icns.
286 *
287 * @param aMimeType the icon mimetype
288 * @param aImageStream the stream for the image data
289 * @param aDir the directory where the icon should be stored
290 */
291 _processIcon: function(aMimeType, aIcon, aDir) {
292 let deferred = Promise.defer();
293
294 function conversionDone(aSubject, aTopic) {
295 if (aTopic == "process-finished") {
296 deferred.resolve();
297 } else {
298 deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue);
299 }
300 }
301
302 let process = Cc["@mozilla.org/process/util;1"].
303 createInstance(Ci.nsIProcess);
304 let sipsFile = getFile("/usr/bin/sips");
305
306 process.init(sipsFile);
307 process.runAsync(["-s", "format", "icns",
308 aIcon.path,
309 "--out", OS.Path.join(aDir, this.iconFile),
310 "-z", "128", "128"],
311 9, conversionDone);
312
313 return deferred.promise;
314 }
315 }

mercurial