|
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
7 |
|
8 const APK_MIME_TYPE = "application/vnd.android.package-archive"; |
|
9 const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; |
|
10 const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download"; |
|
11 |
|
12 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
13 Cu.import("resource://gre/modules/HelperApps.jsm"); |
|
14 Cu.import("resource://gre/modules/Prompt.jsm"); |
|
15 Cu.import("resource://gre/modules/Services.jsm"); |
|
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
17 |
|
18 // ----------------------------------------------------------------------- |
|
19 // HelperApp Launcher Dialog |
|
20 // ----------------------------------------------------------------------- |
|
21 |
|
22 function HelperAppLauncherDialog() { } |
|
23 |
|
24 HelperAppLauncherDialog.prototype = { |
|
25 classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"), |
|
26 QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), |
|
27 |
|
28 getNativeWindow: function () { |
|
29 try { |
|
30 let win = Services.wm.getMostRecentWindow("navigator:browser"); |
|
31 if (win && win.NativeWindow) { |
|
32 return win.NativeWindow; |
|
33 } |
|
34 } catch (e) { |
|
35 } |
|
36 return null; |
|
37 }, |
|
38 |
|
39 /** |
|
40 * Returns false if `url` represents a local or special URL that we don't |
|
41 * wish to ever download. |
|
42 * |
|
43 * Returns true otherwise. |
|
44 */ |
|
45 _canDownload: function (url, alreadyResolved=false) { |
|
46 // The common case. |
|
47 if (url.schemeIs("http") || |
|
48 url.schemeIs("https") || |
|
49 url.schemeIs("ftp")) { |
|
50 return true; |
|
51 } |
|
52 |
|
53 // The less-common opposite case. |
|
54 if (url.schemeIs("chrome") || |
|
55 url.schemeIs("jar") || |
|
56 url.schemeIs("resource") || |
|
57 url.schemeIs("wyciwyg")) { |
|
58 return false; |
|
59 } |
|
60 |
|
61 // For all other URIs, try to resolve them to an inner URI, and check that. |
|
62 if (!alreadyResolved) { |
|
63 let ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); |
|
64 let innerURI = ioSvc.newChannelFromURI(url).URI; |
|
65 if (!url.equals(innerURI)) { |
|
66 return this._canDownload(innerURI, true); |
|
67 } |
|
68 } |
|
69 |
|
70 if (url.schemeIs("file")) { |
|
71 // If it's in our app directory or profile directory, we never ever |
|
72 // want to do anything with it, including saving to disk or passing the |
|
73 // file to another application. |
|
74 let file = url.QueryInterface(Ci.nsIFileURL).file; |
|
75 |
|
76 // Normalize the nsILocalFile in-place. This will ensure that paths |
|
77 // can be correctly compared via `contains`, below. |
|
78 file.normalize(); |
|
79 |
|
80 // TODO: pref blacklist? |
|
81 |
|
82 let appRoot = FileUtils.getFile("XREExeF", []); |
|
83 if (appRoot.contains(file, true)) { |
|
84 return false; |
|
85 } |
|
86 |
|
87 let profileRoot = FileUtils.getFile("ProfD", []); |
|
88 if (profileRoot.contains(file, true)) { |
|
89 return false; |
|
90 } |
|
91 |
|
92 return true; |
|
93 } |
|
94 |
|
95 // Anything else is fine to download. |
|
96 return true; |
|
97 }, |
|
98 |
|
99 /** |
|
100 * Returns true if `launcher` represents a download for which we wish |
|
101 * to prompt. |
|
102 */ |
|
103 _shouldPrompt: function (launcher) { |
|
104 let mimeType = this._getMimeTypeFromLauncher(launcher); |
|
105 |
|
106 // Straight equality: nsIMIMEInfo normalizes. |
|
107 return APK_MIME_TYPE == mimeType; |
|
108 }, |
|
109 |
|
110 show: function hald_show(aLauncher, aContext, aReason) { |
|
111 if (!this._canDownload(aLauncher.source)) { |
|
112 aLauncher.cancel(Cr.NS_BINDING_ABORTED); |
|
113 |
|
114 let win = this.getNativeWindow(); |
|
115 if (!win) { |
|
116 // Oops. |
|
117 Services.console.logStringMessage("Refusing download, but can't show a toast."); |
|
118 return; |
|
119 } |
|
120 |
|
121 Services.console.logStringMessage("Refusing download of non-downloadable file."); |
|
122 let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties"); |
|
123 let failedText = bundle.GetStringFromName("protocol.failed"); |
|
124 win.toast.show(failedText, "long"); |
|
125 |
|
126 return; |
|
127 } |
|
128 |
|
129 let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); |
|
130 |
|
131 let defaultHandler = new Object(); |
|
132 let apps = HelperApps.getAppsForUri(aLauncher.source, { |
|
133 mimeType: aLauncher.MIMEInfo.MIMEType, |
|
134 }); |
|
135 |
|
136 // Add a fake intent for save to disk at the top of the list. |
|
137 apps.unshift({ |
|
138 name: bundle.GetStringFromName("helperapps.saveToDisk"), |
|
139 packageName: "org.mozilla.gecko.Download", |
|
140 iconUri: "drawable://icon", |
|
141 launch: function() { |
|
142 // Reset the preferredAction here. |
|
143 aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk; |
|
144 aLauncher.saveToDisk(null, false); |
|
145 return true; |
|
146 } |
|
147 }); |
|
148 |
|
149 // See if the user already marked something as the default for this mimetype, |
|
150 // and if that app is still installed. |
|
151 let preferredApp = this._getPreferredApp(aLauncher); |
|
152 if (preferredApp) { |
|
153 let pref = apps.filter(function(app) { |
|
154 return app.packageName === preferredApp; |
|
155 }); |
|
156 |
|
157 if (pref.length > 0) { |
|
158 pref[0].launch(aLauncher.source); |
|
159 return; |
|
160 } |
|
161 } |
|
162 |
|
163 let callback = function(app) { |
|
164 aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; |
|
165 if (!app.launch(aLauncher.source)) { |
|
166 aLauncher.cancel(Cr.NS_BINDING_ABORTED); |
|
167 } |
|
168 } |
|
169 |
|
170 // If there's only one choice, and we don't want to prompt, go right ahead |
|
171 // and choose that app automatically. |
|
172 if (!this._shouldPrompt(aLauncher) && (apps.length === 1)) { |
|
173 callback(apps[0]); |
|
174 return; |
|
175 } |
|
176 |
|
177 // Otherwise, let's go through the prompt. |
|
178 HelperApps.prompt(apps, { |
|
179 title: bundle.GetStringFromName("helperapps.pick"), |
|
180 buttons: [ |
|
181 bundle.GetStringFromName("helperapps.alwaysUse"), |
|
182 bundle.GetStringFromName("helperapps.useJustOnce") |
|
183 ] |
|
184 }, (data) => { |
|
185 if (data.button < 0) { |
|
186 return; |
|
187 } |
|
188 |
|
189 callback(apps[data.icongrid0]); |
|
190 |
|
191 if (data.button === 0) { |
|
192 this._setPreferredApp(aLauncher, apps[data.icongrid0]); |
|
193 } |
|
194 }); |
|
195 }, |
|
196 |
|
197 _getPrefName: function getPrefName(mimetype) { |
|
198 return "browser.download.preferred." + mimetype.replace("\\", "."); |
|
199 }, |
|
200 |
|
201 _getMimeTypeFromLauncher: function (launcher) { |
|
202 let mime = launcher.MIMEInfo.MIMEType; |
|
203 if (!mime) |
|
204 mime = ContentAreaUtils.getMIMETypeForURI(launcher.source) || ""; |
|
205 return mime; |
|
206 }, |
|
207 |
|
208 _getPreferredApp: function getPreferredApp(launcher) { |
|
209 let mime = this._getMimeTypeFromLauncher(launcher); |
|
210 if (!mime) |
|
211 return; |
|
212 |
|
213 try { |
|
214 return Services.prefs.getCharPref(this._getPrefName(mime)); |
|
215 } catch(ex) { |
|
216 Services.console.logStringMessage("Error getting pref for " + mime + "."); |
|
217 } |
|
218 return null; |
|
219 }, |
|
220 |
|
221 _setPreferredApp: function setPreferredApp(launcher, app) { |
|
222 let mime = this._getMimeTypeFromLauncher(launcher); |
|
223 if (!mime) |
|
224 return; |
|
225 |
|
226 if (app) |
|
227 Services.prefs.setCharPref(this._getPrefName(mime), app.packageName); |
|
228 else |
|
229 Services.prefs.clearUserPref(this._getPrefName(mime)); |
|
230 }, |
|
231 |
|
232 promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { |
|
233 // Retrieve the user's default download directory |
|
234 let dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); |
|
235 let defaultFolder = dnldMgr.userDownloadsDirectory; |
|
236 |
|
237 try { |
|
238 file = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExt); |
|
239 } catch (e) { } |
|
240 |
|
241 return file; |
|
242 }, |
|
243 |
|
244 validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) { |
|
245 if (!(aLocalFile && this.isUsableDirectory(aLocalFile))) |
|
246 return null; |
|
247 |
|
248 // Remove any leading periods, since we don't want to save hidden files |
|
249 // automatically. |
|
250 aLeafName = aLeafName.replace(/^\.+/, ""); |
|
251 |
|
252 if (aLeafName == "") |
|
253 aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); |
|
254 aLocalFile.append(aLeafName); |
|
255 |
|
256 this.makeFileUnique(aLocalFile); |
|
257 return aLocalFile; |
|
258 }, |
|
259 |
|
260 makeFileUnique: function hald_makeFileUnique(aLocalFile) { |
|
261 try { |
|
262 // Note - this code is identical to that in |
|
263 // toolkit/content/contentAreaUtils.js. |
|
264 // If you are updating this code, update that code too! We can't share code |
|
265 // here since this is called in a js component. |
|
266 let collisionCount = 0; |
|
267 while (aLocalFile.exists()) { |
|
268 collisionCount++; |
|
269 if (collisionCount == 1) { |
|
270 // Append "(2)" before the last dot in (or at the end of) the filename |
|
271 // special case .ext.gz etc files so we don't wind up with .tar(2).gz |
|
272 if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) |
|
273 aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); |
|
274 else |
|
275 aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); |
|
276 } |
|
277 else { |
|
278 // replace the last (n) in the filename with (n+1) |
|
279 aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")"); |
|
280 } |
|
281 } |
|
282 aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); |
|
283 } |
|
284 catch (e) { |
|
285 dump("*** exception in validateLeafName: " + e + "\n"); |
|
286 |
|
287 if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) |
|
288 throw e; |
|
289 |
|
290 if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) { |
|
291 aLocalFile.append("unnamed"); |
|
292 if (aLocalFile.exists()) |
|
293 aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); |
|
294 } |
|
295 } |
|
296 }, |
|
297 |
|
298 isUsableDirectory: function hald_isUsableDirectory(aDirectory) { |
|
299 return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable(); |
|
300 }, |
|
301 }; |
|
302 |
|
303 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]); |