|
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 /** |
|
6 * Constructor for the Linux native app shell |
|
7 * |
|
8 * @param aApp {Object} the app object provided to the install function |
|
9 * @param aManifest {Object} the manifest data provided by the web app |
|
10 * @param aCategories {Array} array of app categories |
|
11 * @param aRegistryDir {String} (optional) path to the registry |
|
12 */ |
|
13 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { |
|
14 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); |
|
15 |
|
16 this.iconFile = "icon.png"; |
|
17 this.webapprt = "webapprt-stub"; |
|
18 this.configJson = "webapp.json"; |
|
19 this.webappINI = "webapp.ini"; |
|
20 this.zipFile = "application.zip"; |
|
21 |
|
22 this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ]; |
|
23 if (this.isPackaged) { |
|
24 this.backupFiles.push(this.zipFile); |
|
25 } |
|
26 |
|
27 let xdg_data_home = Cc["@mozilla.org/process/environment;1"]. |
|
28 getService(Ci.nsIEnvironment). |
|
29 get("XDG_DATA_HOME"); |
|
30 if (!xdg_data_home) { |
|
31 xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share"); |
|
32 } |
|
33 |
|
34 // The desktop file name is: "owa-" + sanitized app name + |
|
35 // "-" + manifest url hash. |
|
36 this.desktopINI = OS.Path.join(xdg_data_home, "applications", |
|
37 "owa-" + this.uniqueName + ".desktop"); |
|
38 } |
|
39 |
|
40 NativeApp.prototype = { |
|
41 __proto__: CommonNativeApp.prototype, |
|
42 |
|
43 /** |
|
44 * Creates a native installation of the web app in the OS |
|
45 * |
|
46 * @param aManifest {Object} the manifest data provided by the web app |
|
47 * @param aZipPath {String} path to the zip file for packaged apps (undefined |
|
48 * for hosted apps) |
|
49 */ |
|
50 install: Task.async(function*(aManifest, aZipPath) { |
|
51 if (this._dryRun) { |
|
52 return; |
|
53 } |
|
54 |
|
55 // If the application is already installed, this is a reinstallation. |
|
56 if (WebappOSUtils.getInstallPath(this.app)) { |
|
57 return yield this.prepareUpdate(aManifest, aZipPath); |
|
58 } |
|
59 |
|
60 this._setData(aManifest); |
|
61 |
|
62 // The installation directory name is: sanitized app name + |
|
63 // "-" + manifest url hash. |
|
64 let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName); |
|
65 |
|
66 let dir = getFile(TMP_DIR, this.uniqueName); |
|
67 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
|
68 let tmpDir = dir.path; |
|
69 |
|
70 // Create the installation in a temporary directory. |
|
71 try { |
|
72 this._copyPrebuiltFiles(tmpDir); |
|
73 yield this._createConfigFiles(tmpDir); |
|
74 |
|
75 if (aZipPath) { |
|
76 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); |
|
77 } |
|
78 |
|
79 yield this._getIcon(tmpDir); |
|
80 } catch (ex) { |
|
81 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
|
82 throw ex; |
|
83 } |
|
84 |
|
85 // Apply the installation. |
|
86 this._removeInstallation(true, installDir); |
|
87 |
|
88 try { |
|
89 yield this._applyTempInstallation(tmpDir, installDir); |
|
90 } catch (ex) { |
|
91 this._removeInstallation(false, installDir); |
|
92 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
|
93 throw ex; |
|
94 } |
|
95 }), |
|
96 |
|
97 /** |
|
98 * Creates an update in a temporary directory to be applied later. |
|
99 * |
|
100 * @param aManifest {Object} the manifest data provided by the web app |
|
101 * @param aZipPath {String} path to the zip file for packaged apps (undefined |
|
102 * for hosted apps) |
|
103 */ |
|
104 prepareUpdate: Task.async(function*(aManifest, aZipPath) { |
|
105 if (this._dryRun) { |
|
106 return; |
|
107 } |
|
108 |
|
109 this._setData(aManifest); |
|
110 |
|
111 let installDir = WebappOSUtils.getInstallPath(this.app); |
|
112 if (!installDir) { |
|
113 throw ERR_NOT_INSTALLED; |
|
114 } |
|
115 |
|
116 let baseName = OS.Path.basename(installDir) |
|
117 let oldUniqueName = baseName.substring(1, baseName.length); |
|
118 if (this.uniqueName != oldUniqueName) { |
|
119 // Bug 919799: If the app is still in the registry, migrate its data to |
|
120 // the new format. |
|
121 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; |
|
122 } |
|
123 |
|
124 let updateDir = OS.Path.join(installDir, "update"); |
|
125 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
126 yield OS.File.makeDir(updateDir); |
|
127 |
|
128 try { |
|
129 yield this._createConfigFiles(updateDir); |
|
130 |
|
131 if (aZipPath) { |
|
132 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); |
|
133 } |
|
134 |
|
135 yield this._getIcon(updateDir); |
|
136 } catch (ex) { |
|
137 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
138 throw ex; |
|
139 } |
|
140 }), |
|
141 |
|
142 /** |
|
143 * Applies an update. |
|
144 */ |
|
145 applyUpdate: Task.async(function*() { |
|
146 if (this._dryRun) { |
|
147 return; |
|
148 } |
|
149 |
|
150 let installDir = WebappOSUtils.getInstallPath(this.app); |
|
151 let updateDir = OS.Path.join(installDir, "update"); |
|
152 |
|
153 let backupDir = yield this._backupInstallation(installDir); |
|
154 |
|
155 try { |
|
156 yield this._applyTempInstallation(updateDir, installDir); |
|
157 } catch (ex) { |
|
158 yield this._restoreInstallation(backupDir, installDir); |
|
159 throw ex; |
|
160 } finally { |
|
161 yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
|
162 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
163 } |
|
164 }), |
|
165 |
|
166 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { |
|
167 yield moveDirectory(aTmpDir, aInstallDir); |
|
168 |
|
169 this._createSystemFiles(aInstallDir); |
|
170 }), |
|
171 |
|
172 _removeInstallation: function(keepProfile, aInstallDir) { |
|
173 let filesToRemove = [this.desktopINI]; |
|
174 |
|
175 if (keepProfile) { |
|
176 for (let filePath of this.backupFiles) { |
|
177 filesToRemove.push(OS.Path.join(aInstallDir, filePath)); |
|
178 } |
|
179 |
|
180 filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); |
|
181 } else { |
|
182 filesToRemove.push(aInstallDir); |
|
183 } |
|
184 |
|
185 removeFiles(filesToRemove); |
|
186 }, |
|
187 |
|
188 _backupInstallation: Task.async(function*(aInstallDir) { |
|
189 let backupDir = OS.Path.join(aInstallDir, "backup"); |
|
190 yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
|
191 yield OS.File.makeDir(backupDir); |
|
192 |
|
193 for (let filePath of this.backupFiles) { |
|
194 yield OS.File.move(OS.Path.join(aInstallDir, filePath), |
|
195 OS.Path.join(backupDir, filePath)); |
|
196 } |
|
197 |
|
198 return backupDir; |
|
199 }), |
|
200 |
|
201 _restoreInstallation: function(aBackupDir, aInstallDir) { |
|
202 return moveDirectory(aBackupDir, aInstallDir); |
|
203 }, |
|
204 |
|
205 _copyPrebuiltFiles: function(aDir) { |
|
206 let destDir = getFile(aDir); |
|
207 let stub = getFile(this.runtimeFolder, this.webapprt); |
|
208 stub.copyTo(destDir, null); |
|
209 }, |
|
210 |
|
211 /** |
|
212 * Translate marketplace categories to freedesktop.org categories. |
|
213 * |
|
214 * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry |
|
215 * |
|
216 * @return an array of categories |
|
217 */ |
|
218 _translateCategories: function() { |
|
219 let translations = { |
|
220 "books": "Education;Literature", |
|
221 "business": "Finance", |
|
222 "education": "Education", |
|
223 "entertainment": "Amusement", |
|
224 "sports": "Sports", |
|
225 "games": "Game", |
|
226 "health-fitness": "MedicalSoftware", |
|
227 "lifestyle": "Amusement", |
|
228 "music": "Audio;Music", |
|
229 "news-weather": "News", |
|
230 "photo-video": "Video;AudioVideo;Photography", |
|
231 "productivity": "Office", |
|
232 "shopping": "Amusement", |
|
233 "social": "Chat", |
|
234 "travel": "Amusement", |
|
235 "reference": "Science;Education;Documentation", |
|
236 "maps-navigation": "Maps", |
|
237 "utilities": "Utility" |
|
238 }; |
|
239 |
|
240 // The trailing semicolon is needed as written in the freedesktop specification |
|
241 let categories = ""; |
|
242 for (let category of this.categories) { |
|
243 let catLower = category.toLowerCase(); |
|
244 if (catLower in translations) { |
|
245 categories += translations[catLower] + ";"; |
|
246 } |
|
247 } |
|
248 |
|
249 return categories; |
|
250 }, |
|
251 |
|
252 _createConfigFiles: function(aDir) { |
|
253 // ${InstallDir}/webapp.json |
|
254 yield writeToFile(OS.Path.join(aDir, this.configJson), |
|
255 JSON.stringify(this.webappJson)); |
|
256 |
|
257 let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); |
|
258 |
|
259 // ${InstallDir}/webapp.ini |
|
260 let webappINIfile = getFile(aDir, this.webappINI); |
|
261 |
|
262 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. |
|
263 getService(Ci.nsIINIParserFactory). |
|
264 createINIParser(webappINIfile). |
|
265 QueryInterface(Ci.nsIINIParserWriter); |
|
266 writer.setString("Webapp", "Name", this.appName); |
|
267 writer.setString("Webapp", "Profile", this.uniqueName); |
|
268 writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1)); |
|
269 writer.setString("WebappRT", "InstallDir", this.runtimeFolder); |
|
270 writer.writeFile(); |
|
271 }, |
|
272 |
|
273 _createSystemFiles: function(aInstallDir) { |
|
274 let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); |
|
275 |
|
276 let webapprtPath = OS.Path.join(aInstallDir, this.webapprt); |
|
277 |
|
278 // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop |
|
279 let desktopINIfile = getFile(this.desktopINI); |
|
280 if (desktopINIfile.parent && !desktopINIfile.parent.exists()) { |
|
281 desktopINIfile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
|
282 } |
|
283 |
|
284 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. |
|
285 getService(Ci.nsIINIParserFactory). |
|
286 createINIParser(desktopINIfile). |
|
287 QueryInterface(Ci.nsIINIParserWriter); |
|
288 writer.setString("Desktop Entry", "Name", this.appName); |
|
289 writer.setString("Desktop Entry", "Comment", this.shortDescription); |
|
290 writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"'); |
|
291 writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir, |
|
292 this.iconFile)); |
|
293 writer.setString("Desktop Entry", "Type", "Application"); |
|
294 writer.setString("Desktop Entry", "Terminal", "false"); |
|
295 |
|
296 let categories = this._translateCategories(); |
|
297 if (categories) |
|
298 writer.setString("Desktop Entry", "Categories", categories); |
|
299 |
|
300 writer.setString("Desktop Entry", "Actions", "Uninstall;"); |
|
301 writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label")); |
|
302 writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove"); |
|
303 |
|
304 writer.writeFile(); |
|
305 |
|
306 desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR; |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Process the icon from the imageStream as retrieved from |
|
311 * the URL by getIconForApp(). |
|
312 * |
|
313 * @param aMimeType ahe icon mimetype |
|
314 * @param aImageStream the stream for the image data |
|
315 * @param aDir the directory where the icon should be stored |
|
316 */ |
|
317 _processIcon: function(aMimeType, aImageStream, aDir) { |
|
318 let deferred = Promise.defer(); |
|
319 |
|
320 let imgTools = Cc["@mozilla.org/image/tools;1"]. |
|
321 createInstance(Ci.imgITools); |
|
322 |
|
323 let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); |
|
324 let iconStream = imgTools.encodeImage(imgContainer, "image/png"); |
|
325 |
|
326 let iconFile = getFile(aDir, this.iconFile); |
|
327 let outputStream = FileUtils.openSafeFileOutputStream(iconFile); |
|
328 NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { |
|
329 if (Components.isSuccessCode(aResult)) { |
|
330 deferred.resolve(); |
|
331 } else { |
|
332 deferred.reject("Failure copying icon: " + aResult); |
|
333 } |
|
334 }); |
|
335 |
|
336 return deferred.promise; |
|
337 } |
|
338 } |