|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Cc = Components.classes; |
|
6 const Ci = Components.interfaces; |
|
7 const Cu = Components.utils; |
|
8 const Cr = Components.results; |
|
9 |
|
10 const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; |
|
11 const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png"; |
|
12 |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 Cu.import("resource://gre/modules/DownloadUtils.jsm"); |
|
16 |
|
17 XPCOMUtils.defineLazyGetter(this, "ContentUtil", function() { |
|
18 Cu.import("resource:///modules/ContentUtil.jsm"); |
|
19 return ContentUtil; |
|
20 }); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
22 "resource://gre/modules/Downloads.jsm"); |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
24 "resource://gre/modules/FileUtils.jsm"); |
|
25 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
26 "resource://gre/modules/Task.jsm"); |
|
27 |
|
28 // ----------------------------------------------------------------------- |
|
29 // HelperApp Launcher Dialog |
|
30 // ----------------------------------------------------------------------- |
|
31 |
|
32 function HelperAppLauncherDialog() { } |
|
33 |
|
34 HelperAppLauncherDialog.prototype = { |
|
35 classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"), |
|
36 QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), |
|
37 |
|
38 show: function hald_show(aLauncher, aContext, aReason) { |
|
39 // Check to see if we can open this file or not |
|
40 // If the file is an executable then launchWithApplication will fail in |
|
41 // /uriloader nsMIMEInfoWin.cpp code. So always download in that case. |
|
42 if (aLauncher.MIMEInfo.hasDefaultHandler && !aLauncher.targetFileIsExecutable) { |
|
43 aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault; |
|
44 aLauncher.launchWithApplication(null, false); |
|
45 } else { |
|
46 let wasClicked = false; |
|
47 this._showDownloadInfobar(aLauncher); |
|
48 } |
|
49 }, |
|
50 |
|
51 _getDownloadSize: function dv__getDownloadSize (aSize) { |
|
52 let displaySize = DownloadUtils.convertByteUnits(aSize); |
|
53 // displaySize[0] is formatted size, displaySize[1] is units |
|
54 if (aSize > 0) |
|
55 return displaySize.join(""); |
|
56 else { |
|
57 let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); |
|
58 return browserBundle.GetStringFromName("downloadsUnknownSize"); |
|
59 } |
|
60 }, |
|
61 |
|
62 _getChromeWindow: function (aWindow) { |
|
63 let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
64 .getInterface(Ci.nsIWebNavigation) |
|
65 .QueryInterface(Ci.nsIDocShellTreeItem) |
|
66 .rootTreeItem |
|
67 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
68 .getInterface(Ci.nsIDOMWindow) |
|
69 .QueryInterface(Ci.nsIDOMChromeWindow); |
|
70 return chromeWin; |
|
71 }, |
|
72 |
|
73 _showDownloadInfobar: function do_showDownloadInfobar(aLauncher) { |
|
74 let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); |
|
75 |
|
76 let runButtonText = |
|
77 browserBundle.GetStringFromName("downloadOpen"); |
|
78 let saveButtonText = |
|
79 browserBundle.GetStringFromName("downloadSave"); |
|
80 let cancelButtonText = |
|
81 browserBundle.GetStringFromName("downloadCancel"); |
|
82 |
|
83 let buttons = [ |
|
84 { |
|
85 isDefault: true, |
|
86 label: runButtonText, |
|
87 accessKey: "", |
|
88 callback: function() { |
|
89 aLauncher.saveToDisk(null, false); |
|
90 Services.obs.notifyObservers(aLauncher.targetFile, "dl-run", "true"); |
|
91 } |
|
92 }, |
|
93 { |
|
94 label: saveButtonText, |
|
95 accessKey: "", |
|
96 callback: function() { |
|
97 aLauncher.saveToDisk(null, false); |
|
98 Services.obs.notifyObservers(aLauncher.targetFile, "dl-run", "false"); |
|
99 } |
|
100 }, |
|
101 { |
|
102 label: cancelButtonText, |
|
103 accessKey: "", |
|
104 callback: function() { aLauncher.cancel(Cr.NS_BINDING_ABORTED); } |
|
105 } |
|
106 ]; |
|
107 |
|
108 let window = Services.wm.getMostRecentWindow("navigator:browser"); |
|
109 let chromeWin = this._getChromeWindow(window).wrappedJSObject; |
|
110 let notificationBox = chromeWin.Browser.getNotificationBox(); |
|
111 let document = notificationBox.ownerDocument; |
|
112 let downloadSize = this._getDownloadSize(aLauncher.contentLength); |
|
113 |
|
114 let msg = browserBundle.GetStringFromName("alertDownloadSave2"); |
|
115 |
|
116 let fragment = ContentUtil.populateFragmentFromString( |
|
117 document.createDocumentFragment(), |
|
118 msg, |
|
119 { |
|
120 text: aLauncher.suggestedFileName, |
|
121 className: "download-filename-text" |
|
122 }, |
|
123 { |
|
124 text: downloadSize, |
|
125 className: "download-size-text" |
|
126 }, |
|
127 { |
|
128 text: aLauncher.source.host, |
|
129 className: "download-host-text" |
|
130 } |
|
131 ); |
|
132 let newBar = notificationBox.appendNotification("", |
|
133 "save-download", |
|
134 URI_GENERIC_ICON_DOWNLOAD, |
|
135 notificationBox.PRIORITY_WARNING_HIGH, |
|
136 buttons); |
|
137 let messageContainer = document.getAnonymousElementByAttribute(newBar, "anonid", "messageText"); |
|
138 messageContainer.appendChild(fragment); |
|
139 }, |
|
140 |
|
141 promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { |
|
142 throw new Components.Exception("Async version must be used", Cr.NS_ERROR_NOT_AVAILABLE); |
|
143 }, |
|
144 |
|
145 promptForSaveToFileAsync: function hald_promptForSaveToFileAsync(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { |
|
146 let file = null; |
|
147 let prefs = Services.prefs; |
|
148 |
|
149 Task.spawn(function() { |
|
150 if (!aForcePrompt) { |
|
151 // Check to see if the user wishes to auto save to the default download |
|
152 // folder without prompting. Note that preference might not be set. |
|
153 let autodownload = true; |
|
154 try { |
|
155 autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR); |
|
156 } catch (e) { } |
|
157 |
|
158 if (autodownload) { |
|
159 // Retrieve the user's preferred download directory |
|
160 let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); |
|
161 let defaultFolder = new FileUtils.File(preferredDir); |
|
162 |
|
163 try { |
|
164 file = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExt); |
|
165 } |
|
166 catch (e) { |
|
167 } |
|
168 |
|
169 // Check to make sure we have a valid directory, otherwise, prompt |
|
170 if (file) { |
|
171 aLauncher.saveDestinationAvailable(file); |
|
172 return; |
|
173 } |
|
174 } |
|
175 } |
|
176 |
|
177 // Use file picker to show dialog. |
|
178 let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
|
179 let windowTitle = ""; |
|
180 let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); |
|
181 picker.init(parent, windowTitle, Ci.nsIFilePicker.modeSave); |
|
182 picker.defaultString = aDefaultFile; |
|
183 |
|
184 if (aSuggestedFileExt) { |
|
185 // aSuggestedFileExtension includes the period, so strip it |
|
186 picker.defaultExtension = aSuggestedFileExt.substring(1); |
|
187 } |
|
188 else { |
|
189 try { |
|
190 picker.defaultExtension = aLauncher.MIMEInfo.primaryExtension; |
|
191 } |
|
192 catch (e) { } |
|
193 } |
|
194 |
|
195 let wildCardExtension = "*"; |
|
196 if (aSuggestedFileExt) { |
|
197 wildCardExtension += aSuggestedFileExt; |
|
198 picker.appendFilter(aLauncher.MIMEInfo.description, wildCardExtension); |
|
199 } |
|
200 |
|
201 picker.appendFilters(Ci.nsIFilePicker.filterAll); |
|
202 |
|
203 // Default to lastDir if it is valid, otherwise use the user's preferred |
|
204 // downloads directory. getPreferredDownloadsDirectory should always |
|
205 // return a valid directory string, so we can safely default to it. |
|
206 let preferredDir = yield Downloads.getPreferredDownloadsDirectory(); |
|
207 picker.displayDirectory = new FileUtils.File(preferredDir); |
|
208 |
|
209 // The last directory preference may not exist, which will throw. |
|
210 try { |
|
211 let lastDir = prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile); |
|
212 if (isUsableDirectory(lastDir)) |
|
213 picker.displayDirectory = lastDir; |
|
214 } |
|
215 catch (e) { } |
|
216 |
|
217 picker.open(function(aResult) { |
|
218 if (aResult == Ci.nsIFilePicker.returnCancel) { |
|
219 // null result means user cancelled. |
|
220 aLauncher.saveDestinationAvailable(null); |
|
221 return; |
|
222 } |
|
223 |
|
224 // Be sure to save the directory the user chose through the Save As... |
|
225 // dialog as the new browser.download.dir since the old one |
|
226 // didn't exist. |
|
227 file = picker.file; |
|
228 |
|
229 if (file) { |
|
230 try { |
|
231 // Remove the file so that it's not there when we ensure non-existence later; |
|
232 // this is safe because for the file to exist, the user would have had to |
|
233 // confirm that he wanted the file overwritten. |
|
234 if (file.exists()) |
|
235 file.remove(false); |
|
236 } |
|
237 catch (e) { } |
|
238 let newDir = file.parent.QueryInterface(Ci.nsILocalFile); |
|
239 prefs.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, newDir); |
|
240 file = this.validateLeafName(newDir, file.leafName, null); |
|
241 } |
|
242 aLauncher.saveDestinationAvailable(file); |
|
243 }.bind(this)); |
|
244 }.bind(this)); |
|
245 }, |
|
246 |
|
247 validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) { |
|
248 if (!(aLocalFile && this.isUsableDirectory(aLocalFile))) |
|
249 return null; |
|
250 |
|
251 // Remove any leading periods, since we don't want to save hidden files |
|
252 // automatically. |
|
253 aLeafName = aLeafName.replace(/^\.+/, ""); |
|
254 |
|
255 if (aLeafName == "") |
|
256 aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); |
|
257 aLocalFile.append(aLeafName); |
|
258 |
|
259 this.makeFileUnique(aLocalFile); |
|
260 return aLocalFile; |
|
261 }, |
|
262 |
|
263 makeFileUnique: function hald_makeFileUnique(aLocalFile) { |
|
264 try { |
|
265 // Note - this code is identical to that in |
|
266 // toolkit/content/contentAreaUtils.js. |
|
267 // If you are updating this code, update that code too! We can't share code |
|
268 // here since this is called in a js component. |
|
269 var collisionCount = 0; |
|
270 while (aLocalFile.exists()) { |
|
271 collisionCount++; |
|
272 if (collisionCount == 1) { |
|
273 // Append "(2)" before the last dot in (or at the end of) the filename |
|
274 // special case .ext.gz etc files so we don't wind up with .tar(2).gz |
|
275 if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) |
|
276 aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); |
|
277 else |
|
278 aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); |
|
279 } |
|
280 else { |
|
281 // replace the last (n) in the filename with (n+1) |
|
282 aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")"); |
|
283 } |
|
284 } |
|
285 aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); |
|
286 } |
|
287 catch (e) { |
|
288 dump("*** exception in validateLeafName: " + e + "\n"); |
|
289 |
|
290 if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) |
|
291 throw e; |
|
292 |
|
293 if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) { |
|
294 aLocalFile.append("unnamed"); |
|
295 if (aLocalFile.exists()) |
|
296 aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); |
|
297 } |
|
298 } |
|
299 }, |
|
300 |
|
301 isUsableDirectory: function hald_isUsableDirectory(aDirectory) { |
|
302 return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable(); |
|
303 }, |
|
304 }; |
|
305 |
|
306 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]); |