|
1 # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
|
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 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
7 |
|
8 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", |
|
9 "resource://gre/modules/BrowserUtils.jsm"); |
|
10 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
11 "resource://gre/modules/Downloads.jsm"); |
|
12 XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir", |
|
13 "resource://gre/modules/DownloadLastDir.jsm"); |
|
14 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
15 "resource://gre/modules/FileUtils.jsm"); |
|
16 XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
17 "resource://gre/modules/osfile.jsm"); |
|
18 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
19 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
20 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
21 "resource://gre/modules/Promise.jsm"); |
|
22 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
23 "resource://gre/modules/Services.jsm"); |
|
24 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
25 "resource://gre/modules/Task.jsm"); |
|
26 var ContentAreaUtils = { |
|
27 |
|
28 // this is for backwards compatibility. |
|
29 get ioService() { |
|
30 return Services.io; |
|
31 }, |
|
32 |
|
33 get stringBundle() { |
|
34 delete this.stringBundle; |
|
35 return this.stringBundle = |
|
36 Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties"); |
|
37 } |
|
38 } |
|
39 |
|
40 function urlSecurityCheck(aURL, aPrincipal, aFlags) |
|
41 { |
|
42 return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); |
|
43 } |
|
44 |
|
45 /** |
|
46 * Determine whether or not a given focused DOMWindow is in the content area. |
|
47 **/ |
|
48 function isContentFrame(aFocusedWindow) |
|
49 { |
|
50 if (!aFocusedWindow) |
|
51 return false; |
|
52 |
|
53 return (aFocusedWindow.top == window.content); |
|
54 } |
|
55 |
|
56 // Clientele: (Make sure you don't break any of these) |
|
57 // - File -> Save Page/Frame As... |
|
58 // - Context -> Save Page/Frame As... |
|
59 // - Context -> Save Link As... |
|
60 // - Alt-Click links in web pages |
|
61 // - Alt-Click links in the UI |
|
62 // |
|
63 // Try saving each of these types: |
|
64 // - A complete webpage using File->Save Page As, and Context->Save Page As |
|
65 // - A webpage as HTML only using the above methods |
|
66 // - A webpage as Text only using the above methods |
|
67 // - An image with an extension (e.g. .jpg) in its file name, using |
|
68 // Context->Save Image As... |
|
69 // - An image without an extension (e.g. a banner ad on cnn.com) using |
|
70 // the above method. |
|
71 // - A linked document using Save Link As... |
|
72 // - A linked document using Alt-click Save Link As... |
|
73 // |
|
74 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, |
|
75 aSkipPrompt, aReferrer, aSourceDocument) |
|
76 { |
|
77 internalSave(aURL, null, aFileName, null, null, aShouldBypassCache, |
|
78 aFilePickerTitleKey, null, aReferrer, aSourceDocument, |
|
79 aSkipPrompt, null); |
|
80 } |
|
81 |
|
82 // Just like saveURL, but will get some info off the image before |
|
83 // calling internalSave |
|
84 // Clientele: (Make sure you don't break any of these) |
|
85 // - Context -> Save Image As... |
|
86 const imgICache = Components.interfaces.imgICache; |
|
87 const nsISupportsCString = Components.interfaces.nsISupportsCString; |
|
88 |
|
89 function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, |
|
90 aSkipPrompt, aReferrer, aDoc) |
|
91 { |
|
92 var contentType = null; |
|
93 var contentDisposition = null; |
|
94 if (!aShouldBypassCache) { |
|
95 try { |
|
96 var imageCache = Components.classes["@mozilla.org/image/tools;1"] |
|
97 .getService(Components.interfaces.imgITools) |
|
98 .getImgCacheForDocument(aDoc); |
|
99 var props = |
|
100 imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null))); |
|
101 if (props) { |
|
102 contentType = props.get("type", nsISupportsCString); |
|
103 contentDisposition = props.get("content-disposition", |
|
104 nsISupportsCString); |
|
105 } |
|
106 } catch (e) { |
|
107 // Failure to get type and content-disposition off the image is non-fatal |
|
108 } |
|
109 } |
|
110 internalSave(aURL, null, aFileName, contentDisposition, contentType, |
|
111 aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, |
|
112 aDoc, aSkipPrompt, null); |
|
113 } |
|
114 |
|
115 function saveDocument(aDocument, aSkipPrompt) |
|
116 { |
|
117 if (!aDocument) |
|
118 throw "Must have a document when calling saveDocument"; |
|
119 |
|
120 // We want to use cached data because the document is currently visible. |
|
121 var ifreq = |
|
122 aDocument.defaultView |
|
123 .QueryInterface(Components.interfaces.nsIInterfaceRequestor); |
|
124 |
|
125 var contentDisposition = null; |
|
126 try { |
|
127 contentDisposition = |
|
128 ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils) |
|
129 .getDocumentMetadata("content-disposition"); |
|
130 } catch (ex) { |
|
131 // Failure to get a content-disposition is ok |
|
132 } |
|
133 |
|
134 var cacheKey = null; |
|
135 try { |
|
136 cacheKey = |
|
137 ifreq.getInterface(Components.interfaces.nsIWebNavigation) |
|
138 .QueryInterface(Components.interfaces.nsIWebPageDescriptor); |
|
139 } catch (ex) { |
|
140 // We might not find it in the cache. Oh, well. |
|
141 } |
|
142 |
|
143 internalSave(aDocument.location.href, aDocument, null, contentDisposition, |
|
144 aDocument.contentType, false, null, null, |
|
145 aDocument.referrer ? makeURI(aDocument.referrer) : null, |
|
146 aDocument, aSkipPrompt, cacheKey); |
|
147 } |
|
148 |
|
149 function DownloadListener(win, transfer) { |
|
150 function makeClosure(name) { |
|
151 return function() { |
|
152 transfer[name].apply(transfer, arguments); |
|
153 } |
|
154 } |
|
155 |
|
156 this.window = win; |
|
157 |
|
158 // Now... we need to forward all calls to our transfer |
|
159 for (var i in transfer) { |
|
160 if (i != "QueryInterface") |
|
161 this[i] = makeClosure(i); |
|
162 } |
|
163 } |
|
164 |
|
165 DownloadListener.prototype = { |
|
166 QueryInterface: function dl_qi(aIID) |
|
167 { |
|
168 if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) || |
|
169 aIID.equals(Components.interfaces.nsIWebProgressListener) || |
|
170 aIID.equals(Components.interfaces.nsIWebProgressListener2) || |
|
171 aIID.equals(Components.interfaces.nsISupports)) { |
|
172 return this; |
|
173 } |
|
174 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
175 }, |
|
176 |
|
177 getInterface: function dl_gi(aIID) |
|
178 { |
|
179 if (aIID.equals(Components.interfaces.nsIAuthPrompt) || |
|
180 aIID.equals(Components.interfaces.nsIAuthPrompt2)) { |
|
181 var ww = |
|
182 Components.classes["@mozilla.org/embedcomp/window-watcher;1"] |
|
183 .getService(Components.interfaces.nsIPromptFactory); |
|
184 return ww.getPrompt(this.window, aIID); |
|
185 } |
|
186 |
|
187 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
188 } |
|
189 } |
|
190 |
|
191 const kSaveAsType_Complete = 0; // Save document with attached objects. |
|
192 // const kSaveAsType_URL = 1; // Save document or URL by itself. |
|
193 const kSaveAsType_Text = 2; // Save document, converting to plain text. |
|
194 |
|
195 /** |
|
196 * internalSave: Used when saving a document or URL. |
|
197 * |
|
198 * If aChosenData is null, this method: |
|
199 * - Determines a local target filename to use |
|
200 * - Prompts the user to confirm the destination filename and save mode |
|
201 * (aContentType affects this) |
|
202 * - [Note] This process involves the parameters aURL, aReferrer (to determine |
|
203 * how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey, |
|
204 * and aSkipPrompt. |
|
205 * |
|
206 * If aChosenData is non-null, this method: |
|
207 * - Uses the provided source URI and save file name |
|
208 * - Saves the document as complete DOM if possible (aDocument present and |
|
209 * right aContentType) |
|
210 * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and |
|
211 * aSkipPrompt are ignored. |
|
212 * |
|
213 * In any case, this method: |
|
214 * - Creates a 'Persist' object (which will perform the saving in the |
|
215 * background) and then starts it. |
|
216 * - [Note] This part of the process only involves the parameters aDocument, |
|
217 * aShouldBypassCache and aReferrer. The source, the save name and the save |
|
218 * mode are the ones determined previously. |
|
219 * |
|
220 * @param aURL |
|
221 * The String representation of the URL of the document being saved |
|
222 * @param aDocument |
|
223 * The document to be saved |
|
224 * @param aDefaultFileName |
|
225 * The caller-provided suggested filename if we don't |
|
226 * find a better one |
|
227 * @param aContentDisposition |
|
228 * The caller-provided content-disposition header to use. |
|
229 * @param aContentType |
|
230 * The caller-provided content-type to use |
|
231 * @param aShouldBypassCache |
|
232 * If true, the document will always be refetched from the server |
|
233 * @param aFilePickerTitleKey |
|
234 * Alternate title for the file picker |
|
235 * @param aChosenData |
|
236 * If non-null this contains an instance of object AutoChosen (see below) |
|
237 * which holds pre-determined data so that the user does not need to be |
|
238 * prompted for a target filename. |
|
239 * @param aReferrer |
|
240 * the referrer URI object (not URL string) to use, or null |
|
241 * if no referrer should be sent. |
|
242 * @param aInitiatingDocument |
|
243 * The document from which the save was initiated. |
|
244 * @param aSkipPrompt [optional] |
|
245 * If set to true, we will attempt to save the file to the |
|
246 * default downloads folder without prompting. |
|
247 * @param aCacheKey [optional] |
|
248 * If set will be passed to saveURI. See nsIWebBrowserPersist for |
|
249 * allowed values. |
|
250 */ |
|
251 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, |
|
252 aContentType, aShouldBypassCache, aFilePickerTitleKey, |
|
253 aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt, |
|
254 aCacheKey) |
|
255 { |
|
256 if (aSkipPrompt == undefined) |
|
257 aSkipPrompt = false; |
|
258 |
|
259 if (aCacheKey == undefined) |
|
260 aCacheKey = null; |
|
261 |
|
262 // Note: aDocument == null when this code is used by save-link-as... |
|
263 var saveMode = GetSaveModeForContentType(aContentType, aDocument); |
|
264 |
|
265 var file, sourceURI, saveAsType; |
|
266 // Find the URI object for aURL and the FileName/Extension to use when saving. |
|
267 // FileName/Extension will be ignored if aChosenData supplied. |
|
268 if (aChosenData) { |
|
269 file = aChosenData.file; |
|
270 sourceURI = aChosenData.uri; |
|
271 saveAsType = kSaveAsType_Complete; |
|
272 |
|
273 continueSave(); |
|
274 } else { |
|
275 var charset = null; |
|
276 if (aDocument) |
|
277 charset = aDocument.characterSet; |
|
278 else if (aReferrer) |
|
279 charset = aReferrer.originCharset; |
|
280 var fileInfo = new FileInfo(aDefaultFileName); |
|
281 initFileInfo(fileInfo, aURL, charset, aDocument, |
|
282 aContentType, aContentDisposition); |
|
283 sourceURI = fileInfo.uri; |
|
284 |
|
285 var fpParams = { |
|
286 fpTitleKey: aFilePickerTitleKey, |
|
287 fileInfo: fileInfo, |
|
288 contentType: aContentType, |
|
289 saveMode: saveMode, |
|
290 saveAsType: kSaveAsType_Complete, |
|
291 file: file |
|
292 }; |
|
293 |
|
294 // Find a URI to use for determining last-downloaded-to directory |
|
295 let relatedURI = aReferrer || sourceURI; |
|
296 |
|
297 promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => { |
|
298 if (!aDialogAccepted) |
|
299 return; |
|
300 |
|
301 saveAsType = fpParams.saveAsType; |
|
302 file = fpParams.file; |
|
303 |
|
304 continueSave(); |
|
305 }).then(null, Components.utils.reportError); |
|
306 } |
|
307 |
|
308 function continueSave() { |
|
309 // XXX We depend on the following holding true in appendFiltersForContentType(): |
|
310 // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. |
|
311 // If we should save as text, the saveAsType is kSaveAsType_Text. |
|
312 var useSaveDocument = aDocument && |
|
313 (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) || |
|
314 ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text))); |
|
315 // If we're saving a document, and are saving either in complete mode or |
|
316 // as converted text, pass the document to the web browser persist component. |
|
317 // If we're just saving the HTML (second option in the list), send only the URI. |
|
318 var persistArgs = { |
|
319 sourceURI : sourceURI, |
|
320 sourceReferrer : aReferrer, |
|
321 sourceDocument : useSaveDocument ? aDocument : null, |
|
322 targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null, |
|
323 targetFile : file, |
|
324 sourceCacheKey : aCacheKey, |
|
325 sourcePostData : aDocument ? getPostData(aDocument) : null, |
|
326 bypassCache : aShouldBypassCache, |
|
327 initiatingWindow : aInitiatingDocument.defaultView |
|
328 }; |
|
329 |
|
330 // Start the actual save process |
|
331 internalPersist(persistArgs); |
|
332 } |
|
333 } |
|
334 |
|
335 /** |
|
336 * internalPersist: Creates a 'Persist' object (which will perform the saving |
|
337 * in the background) and then starts it. |
|
338 * |
|
339 * @param persistArgs.sourceURI |
|
340 * The nsIURI of the document being saved |
|
341 * @param persistArgs.sourceCacheKey [optional] |
|
342 * If set will be passed to saveURI |
|
343 * @param persistArgs.sourceDocument [optional] |
|
344 * The document to be saved, or null if not saving a complete document |
|
345 * @param persistArgs.sourceReferrer |
|
346 * Required and used only when persistArgs.sourceDocument is NOT present, |
|
347 * the nsIURI of the referrer to use, or null if no referrer should be |
|
348 * sent. |
|
349 * @param persistArgs.sourcePostData |
|
350 * Required and used only when persistArgs.sourceDocument is NOT present, |
|
351 * represents the POST data to be sent along with the HTTP request, and |
|
352 * must be null if no POST data should be sent. |
|
353 * @param persistArgs.targetFile |
|
354 * The nsIFile of the file to create |
|
355 * @param persistArgs.targetContentType |
|
356 * Required and used only when persistArgs.sourceDocument is present, |
|
357 * determines the final content type of the saved file, or null to use |
|
358 * the same content type as the source document. Currently only |
|
359 * "text/plain" is meaningful. |
|
360 * @param persistArgs.bypassCache |
|
361 * If true, the document will always be refetched from the server |
|
362 * @param persistArgs.initiatingWindow |
|
363 * The window from which the save operation was initiated. |
|
364 */ |
|
365 function internalPersist(persistArgs) |
|
366 { |
|
367 var persist = makeWebBrowserPersist(); |
|
368 |
|
369 // Calculate persist flags. |
|
370 const nsIWBP = Components.interfaces.nsIWebBrowserPersist; |
|
371 const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES | |
|
372 nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES; |
|
373 if (persistArgs.bypassCache) |
|
374 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; |
|
375 else |
|
376 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; |
|
377 |
|
378 // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): |
|
379 persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; |
|
380 |
|
381 // Find the URI associated with the target file |
|
382 var targetFileURL = makeFileURI(persistArgs.targetFile); |
|
383 |
|
384 var isPrivate = PrivateBrowsingUtils.isWindowPrivate(persistArgs.initiatingWindow); |
|
385 |
|
386 // Create download and initiate it (below) |
|
387 var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer); |
|
388 tr.init(persistArgs.sourceURI, |
|
389 targetFileURL, "", null, null, null, persist, isPrivate); |
|
390 persist.progressListener = new DownloadListener(window, tr); |
|
391 |
|
392 if (persistArgs.sourceDocument) { |
|
393 // Saving a Document, not a URI: |
|
394 var filesFolder = null; |
|
395 if (persistArgs.targetContentType != "text/plain") { |
|
396 // Create the local directory into which to save associated files. |
|
397 filesFolder = persistArgs.targetFile.clone(); |
|
398 |
|
399 var nameWithoutExtension = getFileBaseName(filesFolder.leafName); |
|
400 var filesFolderLeafName = |
|
401 ContentAreaUtils.stringBundle |
|
402 .formatStringFromName("filesFolder", [nameWithoutExtension], 1); |
|
403 |
|
404 filesFolder.leafName = filesFolderLeafName; |
|
405 } |
|
406 |
|
407 var encodingFlags = 0; |
|
408 if (persistArgs.targetContentType == "text/plain") { |
|
409 encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; |
|
410 encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; |
|
411 encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; |
|
412 } |
|
413 else { |
|
414 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; |
|
415 } |
|
416 |
|
417 const kWrapColumn = 80; |
|
418 persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder, |
|
419 persistArgs.targetContentType, encodingFlags, kWrapColumn); |
|
420 } else { |
|
421 let privacyContext = persistArgs.initiatingWindow |
|
422 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
423 .getInterface(Components.interfaces.nsIWebNavigation) |
|
424 .QueryInterface(Components.interfaces.nsILoadContext); |
|
425 persist.saveURI(persistArgs.sourceURI, |
|
426 persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null, |
|
427 targetFileURL, privacyContext); |
|
428 } |
|
429 } |
|
430 |
|
431 /** |
|
432 * Structure for holding info about automatically supplied parameters for |
|
433 * internalSave(...). This allows parameters to be supplied so the user does not |
|
434 * need to be prompted for file info. |
|
435 * @param aFileAutoChosen This is an nsIFile object that has been |
|
436 * pre-determined as the filename for the target to save to |
|
437 * @param aUriAutoChosen This is the nsIURI object for the target |
|
438 */ |
|
439 function AutoChosen(aFileAutoChosen, aUriAutoChosen) { |
|
440 this.file = aFileAutoChosen; |
|
441 this.uri = aUriAutoChosen; |
|
442 } |
|
443 |
|
444 /** |
|
445 * Structure for holding info about a URL and the target filename it should be |
|
446 * saved to. This structure is populated by initFileInfo(...). |
|
447 * @param aSuggestedFileName This is used by initFileInfo(...) when it |
|
448 * cannot 'discover' the filename from the url |
|
449 * @param aFileName The target filename |
|
450 * @param aFileBaseName The filename without the file extension |
|
451 * @param aFileExt The extension of the filename |
|
452 * @param aUri An nsIURI object for the url that is being saved |
|
453 */ |
|
454 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) { |
|
455 this.suggestedFileName = aSuggestedFileName; |
|
456 this.fileName = aFileName; |
|
457 this.fileBaseName = aFileBaseName; |
|
458 this.fileExt = aFileExt; |
|
459 this.uri = aUri; |
|
460 } |
|
461 |
|
462 /** |
|
463 * Determine what the 'default' filename string is, its file extension and the |
|
464 * filename without the extension. This filename is used when prompting the user |
|
465 * for confirmation in the file picker dialog. |
|
466 * @param aFI A FileInfo structure into which we'll put the results of this method. |
|
467 * @param aURL The String representation of the URL of the document being saved |
|
468 * @param aURLCharset The charset of aURL. |
|
469 * @param aDocument The document to be saved |
|
470 * @param aContentType The content type we're saving, if it could be |
|
471 * determined by the caller. |
|
472 * @param aContentDisposition The content-disposition header for the object |
|
473 * we're saving, if it could be determined by the caller. |
|
474 */ |
|
475 function initFileInfo(aFI, aURL, aURLCharset, aDocument, |
|
476 aContentType, aContentDisposition) |
|
477 { |
|
478 try { |
|
479 // Get an nsIURI object from aURL if possible: |
|
480 try { |
|
481 aFI.uri = makeURI(aURL, aURLCharset); |
|
482 // Assuming nsiUri is valid, calling QueryInterface(...) on it will |
|
483 // populate extra object fields (eg filename and file extension). |
|
484 var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL); |
|
485 aFI.fileExt = url.fileExtension; |
|
486 } catch (e) { |
|
487 } |
|
488 |
|
489 // Get the default filename: |
|
490 aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName), |
|
491 aFI.uri, aDocument, aContentDisposition); |
|
492 // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied |
|
493 // if saveURL(...) was the original caller (hence both aContentType and |
|
494 // aDocument are blank). If they were saving a link to a website then make |
|
495 // the extension .htm . |
|
496 if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) { |
|
497 aFI.fileExt = "htm"; |
|
498 aFI.fileBaseName = aFI.fileName; |
|
499 } else { |
|
500 aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); |
|
501 aFI.fileBaseName = getFileBaseName(aFI.fileName); |
|
502 } |
|
503 } catch (e) { |
|
504 } |
|
505 } |
|
506 |
|
507 /** |
|
508 * Given the Filepicker Parameters (aFpP), show the file picker dialog, |
|
509 * prompting the user to confirm (or change) the fileName. |
|
510 * @param aFpP |
|
511 * A structure (see definition in internalSave(...) method) |
|
512 * containing all the data used within this method. |
|
513 * @param aSkipPrompt |
|
514 * If true, attempt to save the file automatically to the user's default |
|
515 * download directory, thus skipping the explicit prompt for a file name, |
|
516 * but only if the associated preference is set. |
|
517 * If false, don't save the file automatically to the user's |
|
518 * default download directory, even if the associated preference |
|
519 * is set, but ask for the target explicitly. |
|
520 * @param aRelatedURI |
|
521 * An nsIURI associated with the download. The last used |
|
522 * directory of the picker is retrieved from/stored in the |
|
523 * Content Pref Service using this URI. |
|
524 * @return Promise |
|
525 * @resolve a boolean. When true, it indicates that the file picker dialog |
|
526 * is accepted. |
|
527 */ |
|
528 function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI) |
|
529 { |
|
530 return Task.spawn(function() { |
|
531 let downloadLastDir = new DownloadLastDir(window); |
|
532 let prefBranch = Services.prefs.getBranch("browser.download."); |
|
533 let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); |
|
534 |
|
535 if (!aSkipPrompt) |
|
536 useDownloadDir = false; |
|
537 |
|
538 // Default to the user's default downloads directory configured |
|
539 // through download prefs. |
|
540 let dirPath = yield Downloads.getPreferredDownloadsDirectory(); |
|
541 let dirExists = yield OS.File.exists(dirPath); |
|
542 let dir = new FileUtils.File(dirPath); |
|
543 |
|
544 if (useDownloadDir && dirExists) { |
|
545 dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName, |
|
546 aFpP.fileInfo.fileExt)); |
|
547 aFpP.file = uniqueFile(dir); |
|
548 throw new Task.Result(true); |
|
549 } |
|
550 |
|
551 // We must prompt for the file name explicitly. |
|
552 // If we must prompt because we were asked to... |
|
553 let deferred = Promise.defer(); |
|
554 if (useDownloadDir) { |
|
555 // Keep async behavior in both branches |
|
556 Services.tm.mainThread.dispatch(function() { |
|
557 deferred.resolve(null); |
|
558 }, Components.interfaces.nsIThread.DISPATCH_NORMAL); |
|
559 } else { |
|
560 downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) { |
|
561 deferred.resolve(aFile); |
|
562 }); |
|
563 } |
|
564 let file = yield deferred.promise; |
|
565 if (file && (yield OS.File.exists(file.path))) { |
|
566 dir = file; |
|
567 dirExists = true; |
|
568 } |
|
569 |
|
570 if (!dirExists) { |
|
571 // Default to desktop. |
|
572 dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile); |
|
573 } |
|
574 |
|
575 let fp = makeFilePicker(); |
|
576 let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; |
|
577 fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), |
|
578 Components.interfaces.nsIFilePicker.modeSave); |
|
579 |
|
580 fp.displayDirectory = dir; |
|
581 fp.defaultExtension = aFpP.fileInfo.fileExt; |
|
582 fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName, |
|
583 aFpP.fileInfo.fileExt); |
|
584 appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt, |
|
585 aFpP.saveMode); |
|
586 |
|
587 // The index of the selected filter is only preserved and restored if there's |
|
588 // more than one filter in addition to "All Files". |
|
589 if (aFpP.saveMode != SAVEMODE_FILEONLY) { |
|
590 try { |
|
591 fp.filterIndex = prefBranch.getIntPref("save_converter_index"); |
|
592 } |
|
593 catch (e) { |
|
594 } |
|
595 } |
|
596 |
|
597 let deferComplete = Promise.defer(); |
|
598 fp.open(function(aResult) { |
|
599 deferComplete.resolve(aResult); |
|
600 }); |
|
601 let result = yield deferComplete.promise; |
|
602 if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) { |
|
603 throw new Task.Result(false); |
|
604 } |
|
605 |
|
606 if (aFpP.saveMode != SAVEMODE_FILEONLY) |
|
607 prefBranch.setIntPref("save_converter_index", fp.filterIndex); |
|
608 |
|
609 // Do not store the last save directory as a pref inside the private browsing mode |
|
610 downloadLastDir.setFile(aRelatedURI, fp.file.parent); |
|
611 |
|
612 fp.file.leafName = validateFileName(fp.file.leafName); |
|
613 |
|
614 aFpP.saveAsType = fp.filterIndex; |
|
615 aFpP.file = fp.file; |
|
616 aFpP.fileURL = fp.fileURL; |
|
617 |
|
618 throw new Task.Result(true); |
|
619 }); |
|
620 } |
|
621 |
|
622 // Since we're automatically downloading, we don't get the file picker's |
|
623 // logic to check for existing files, so we need to do that here. |
|
624 // |
|
625 // Note - this code is identical to that in |
|
626 // mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in |
|
627 // If you are updating this code, update that code too! We can't share code |
|
628 // here since that code is called in a js component. |
|
629 function uniqueFile(aLocalFile) |
|
630 { |
|
631 var collisionCount = 0; |
|
632 while (aLocalFile.exists()) { |
|
633 collisionCount++; |
|
634 if (collisionCount == 1) { |
|
635 // Append "(2)" before the last dot in (or at the end of) the filename |
|
636 // special case .ext.gz etc files so we don't wind up with .tar(2).gz |
|
637 if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) |
|
638 aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); |
|
639 else |
|
640 aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); |
|
641 } |
|
642 else { |
|
643 // replace the last (n) in the filename with (n+1) |
|
644 aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")"); |
|
645 } |
|
646 } |
|
647 return aLocalFile; |
|
648 } |
|
649 |
|
650 #ifdef MOZ_JSDOWNLOADS |
|
651 /** |
|
652 * Download a URL using the new jsdownloads API. |
|
653 * |
|
654 * @param aURL |
|
655 * the url to download |
|
656 * @param [optional] aFileName |
|
657 * the destination file name, if omitted will be obtained from the url. |
|
658 * @param aInitiatingDocument |
|
659 * The document from which the download was initiated. |
|
660 */ |
|
661 function DownloadURL(aURL, aFileName, aInitiatingDocument) { |
|
662 // For private browsing, try to get document out of the most recent browser |
|
663 // window, or provide our own if there's no browser window. |
|
664 let isPrivate = aInitiatingDocument.defaultView |
|
665 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
666 .getInterface(Ci.nsIWebNavigation) |
|
667 .QueryInterface(Ci.nsILoadContext) |
|
668 .usePrivateBrowsing; |
|
669 |
|
670 let fileInfo = new FileInfo(aFileName); |
|
671 initFileInfo(fileInfo, aURL, null, null, null, null); |
|
672 |
|
673 let filepickerParams = { |
|
674 fileInfo: fileInfo, |
|
675 saveMode: SAVEMODE_FILEONLY |
|
676 }; |
|
677 |
|
678 Task.spawn(function* () { |
|
679 let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri); |
|
680 if (!accepted) |
|
681 return; |
|
682 |
|
683 let file = filepickerParams.file; |
|
684 let download = yield Downloads.createDownload({ |
|
685 source: { url: aURL, isPrivate: isPrivate }, |
|
686 target: { path: file.path, partFilePath: file.path + ".part" } |
|
687 }); |
|
688 download.tryToKeepPartialData = true; |
|
689 download.start(); |
|
690 |
|
691 // Add the download to the list, allowing it to be managed. |
|
692 let list = yield Downloads.getList(Downloads.ALL); |
|
693 list.add(download); |
|
694 }).then(null, Components.utils.reportError); |
|
695 } |
|
696 #endif |
|
697 |
|
698 // We have no DOM, and can only save the URL as is. |
|
699 const SAVEMODE_FILEONLY = 0x00; |
|
700 // We have a DOM and can save as complete. |
|
701 const SAVEMODE_COMPLETE_DOM = 0x01; |
|
702 // We have a DOM which we can serialize as text. |
|
703 const SAVEMODE_COMPLETE_TEXT = 0x02; |
|
704 |
|
705 // If we are able to save a complete DOM, the 'save as complete' filter |
|
706 // must be the first filter appended. The 'save page only' counterpart |
|
707 // must be the second filter appended. And the 'save as complete text' |
|
708 // filter must be the third filter appended. |
|
709 function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode) |
|
710 { |
|
711 // The bundle name for saving only a specific content type. |
|
712 var bundleName; |
|
713 // The corresponding filter string for a specific content type. |
|
714 var filterString; |
|
715 |
|
716 // XXX all the cases that are handled explicitly here MUST be handled |
|
717 // in GetSaveModeForContentType to return a non-fileonly filter. |
|
718 switch (aContentType) { |
|
719 case "text/html": |
|
720 bundleName = "WebPageHTMLOnlyFilter"; |
|
721 filterString = "*.htm; *.html"; |
|
722 break; |
|
723 |
|
724 case "application/xhtml+xml": |
|
725 bundleName = "WebPageXHTMLOnlyFilter"; |
|
726 filterString = "*.xht; *.xhtml"; |
|
727 break; |
|
728 |
|
729 case "image/svg+xml": |
|
730 bundleName = "WebPageSVGOnlyFilter"; |
|
731 filterString = "*.svg; *.svgz"; |
|
732 break; |
|
733 |
|
734 case "text/xml": |
|
735 case "application/xml": |
|
736 bundleName = "WebPageXMLOnlyFilter"; |
|
737 filterString = "*.xml"; |
|
738 break; |
|
739 |
|
740 default: |
|
741 if (aSaveMode != SAVEMODE_FILEONLY) |
|
742 throw "Invalid save mode for type '" + aContentType + "'"; |
|
743 |
|
744 var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); |
|
745 if (mimeInfo) { |
|
746 |
|
747 var extEnumerator = mimeInfo.getFileExtensions(); |
|
748 |
|
749 var extString = ""; |
|
750 while (extEnumerator.hasMore()) { |
|
751 var extension = extEnumerator.getNext(); |
|
752 if (extString) |
|
753 extString += "; "; // If adding more than one extension, |
|
754 // separate by semi-colon |
|
755 extString += "*." + extension; |
|
756 } |
|
757 |
|
758 if (extString) |
|
759 aFilePicker.appendFilter(mimeInfo.description, extString); |
|
760 } |
|
761 |
|
762 break; |
|
763 } |
|
764 |
|
765 if (aSaveMode & SAVEMODE_COMPLETE_DOM) { |
|
766 aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), |
|
767 filterString); |
|
768 // We should always offer a choice to save document only if |
|
769 // we allow saving as complete. |
|
770 aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName), |
|
771 filterString); |
|
772 } |
|
773 |
|
774 if (aSaveMode & SAVEMODE_COMPLETE_TEXT) |
|
775 aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText); |
|
776 |
|
777 // Always append the all files (*) filter |
|
778 aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll); |
|
779 } |
|
780 |
|
781 function getPostData(aDocument) |
|
782 { |
|
783 try { |
|
784 // Find the session history entry corresponding to the given document. In |
|
785 // the current implementation, nsIWebPageDescriptor.currentDescriptor always |
|
786 // returns a session history entry. |
|
787 var sessionHistoryEntry = |
|
788 aDocument.defaultView |
|
789 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
790 .getInterface(Components.interfaces.nsIWebNavigation) |
|
791 .QueryInterface(Components.interfaces.nsIWebPageDescriptor) |
|
792 .currentDescriptor |
|
793 .QueryInterface(Components.interfaces.nsISHEntry); |
|
794 return sessionHistoryEntry.postData; |
|
795 } |
|
796 catch (e) { |
|
797 } |
|
798 return null; |
|
799 } |
|
800 |
|
801 function makeWebBrowserPersist() |
|
802 { |
|
803 const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; |
|
804 const persistIID = Components.interfaces.nsIWebBrowserPersist; |
|
805 return Components.classes[persistContractID].createInstance(persistIID); |
|
806 } |
|
807 |
|
808 function makeURI(aURL, aOriginCharset, aBaseURI) |
|
809 { |
|
810 return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI); |
|
811 } |
|
812 |
|
813 function makeFileURI(aFile) |
|
814 { |
|
815 return BrowserUtils.makeFileURI(aFile); |
|
816 } |
|
817 |
|
818 function makeFilePicker() |
|
819 { |
|
820 const fpContractID = "@mozilla.org/filepicker;1"; |
|
821 const fpIID = Components.interfaces.nsIFilePicker; |
|
822 return Components.classes[fpContractID].createInstance(fpIID); |
|
823 } |
|
824 |
|
825 function getMIMEService() |
|
826 { |
|
827 const mimeSvcContractID = "@mozilla.org/mime;1"; |
|
828 const mimeSvcIID = Components.interfaces.nsIMIMEService; |
|
829 const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID); |
|
830 return mimeSvc; |
|
831 } |
|
832 |
|
833 // Given aFileName, find the fileName without the extension on the end. |
|
834 function getFileBaseName(aFileName) |
|
835 { |
|
836 // Remove the file extension from aFileName: |
|
837 return aFileName.replace(/\.[^.]*$/, ""); |
|
838 } |
|
839 |
|
840 function getMIMETypeForURI(aURI) |
|
841 { |
|
842 try { |
|
843 return getMIMEService().getTypeFromURI(aURI); |
|
844 } |
|
845 catch (e) { |
|
846 } |
|
847 return null; |
|
848 } |
|
849 |
|
850 function getMIMEInfoForType(aMIMEType, aExtension) |
|
851 { |
|
852 if (aMIMEType || aExtension) { |
|
853 try { |
|
854 return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); |
|
855 } |
|
856 catch (e) { |
|
857 } |
|
858 } |
|
859 return null; |
|
860 } |
|
861 |
|
862 function getDefaultFileName(aDefaultFileName, aURI, aDocument, |
|
863 aContentDisposition) |
|
864 { |
|
865 // 1) look for a filename in the content-disposition header, if any |
|
866 if (aContentDisposition) { |
|
867 const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; |
|
868 const mhpIID = Components.interfaces.nsIMIMEHeaderParam; |
|
869 const mhp = Components.classes[mhpContractID].getService(mhpIID); |
|
870 var dummy = { value: null }; // Need an out param... |
|
871 var charset = getCharsetforSave(aDocument); |
|
872 |
|
873 var fileName = null; |
|
874 try { |
|
875 fileName = mhp.getParameter(aContentDisposition, "filename", charset, |
|
876 true, dummy); |
|
877 } |
|
878 catch (e) { |
|
879 try { |
|
880 fileName = mhp.getParameter(aContentDisposition, "name", charset, true, |
|
881 dummy); |
|
882 } |
|
883 catch (e) { |
|
884 } |
|
885 } |
|
886 if (fileName) |
|
887 return fileName; |
|
888 } |
|
889 |
|
890 let docTitle; |
|
891 if (aDocument) { |
|
892 // If the document looks like HTML or XML, try to use its original title. |
|
893 docTitle = validateFileName(aDocument.title).trim(); |
|
894 if (docTitle) { |
|
895 let contentType = aDocument.contentType; |
|
896 if (contentType == "application/xhtml+xml" || |
|
897 contentType == "application/xml" || |
|
898 contentType == "image/svg+xml" || |
|
899 contentType == "text/html" || |
|
900 contentType == "text/xml") { |
|
901 // 2) Use the document title |
|
902 return docTitle; |
|
903 } |
|
904 } |
|
905 } |
|
906 |
|
907 try { |
|
908 var url = aURI.QueryInterface(Components.interfaces.nsIURL); |
|
909 if (url.fileName != "") { |
|
910 // 3) Use the actual file name, if present |
|
911 var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] |
|
912 .getService(Components.interfaces.nsITextToSubURI); |
|
913 return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName)); |
|
914 } |
|
915 } catch (e) { |
|
916 // This is something like a data: and so forth URI... no filename here. |
|
917 } |
|
918 |
|
919 if (docTitle) |
|
920 // 4) Use the document title |
|
921 return docTitle; |
|
922 |
|
923 if (aDefaultFileName) |
|
924 // 5) Use the caller-provided name, if any |
|
925 return validateFileName(aDefaultFileName); |
|
926 |
|
927 // 6) If this is a directory, use the last directory name |
|
928 var path = aURI.path.match(/\/([^\/]+)\/$/); |
|
929 if (path && path.length > 1) |
|
930 return validateFileName(path[1]); |
|
931 |
|
932 try { |
|
933 if (aURI.host) |
|
934 // 7) Use the host. |
|
935 return aURI.host; |
|
936 } catch (e) { |
|
937 // Some files have no information at all, like Javascript generated pages |
|
938 } |
|
939 try { |
|
940 // 8) Use the default file name |
|
941 return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName"); |
|
942 } catch (e) { |
|
943 //in case localized string cannot be found |
|
944 } |
|
945 // 9) If all else fails, use "index" |
|
946 return "index"; |
|
947 } |
|
948 |
|
949 function validateFileName(aFileName) |
|
950 { |
|
951 var re = /[\/]+/g; |
|
952 if (navigator.appVersion.indexOf("Windows") != -1) { |
|
953 re = /[\\\/\|]+/g; |
|
954 aFileName = aFileName.replace(/[\"]+/g, "'"); |
|
955 aFileName = aFileName.replace(/[\*\:\?]+/g, " "); |
|
956 aFileName = aFileName.replace(/[\<]+/g, "("); |
|
957 aFileName = aFileName.replace(/[\>]+/g, ")"); |
|
958 } |
|
959 else if (navigator.appVersion.indexOf("Macintosh") != -1) |
|
960 re = /[\:\/]+/g; |
|
961 else if (navigator.appVersion.indexOf("Android") != -1) { |
|
962 // On mobile devices, the filesystem may be very limited in what |
|
963 // it considers valid characters. To avoid errors, we sanitize |
|
964 // conservatively. |
|
965 const dangerousChars = "*?<>|\":/\\[];,+="; |
|
966 var processed = ""; |
|
967 for (var i = 0; i < aFileName.length; i++) |
|
968 processed += aFileName.charCodeAt(i) >= 32 && |
|
969 !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i] |
|
970 : "_"; |
|
971 |
|
972 // Last character should not be a space |
|
973 processed = processed.trim(); |
|
974 |
|
975 // If a large part of the filename has been sanitized, then we |
|
976 // will use a default filename instead |
|
977 if (processed.replace(/_/g, "").length <= processed.length/2) { |
|
978 // We purposefully do not use a localized default filename, |
|
979 // which we could have done using |
|
980 // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") |
|
981 // since it may contain invalid characters. |
|
982 var original = processed; |
|
983 processed = "download"; |
|
984 |
|
985 // Preserve a suffix, if there is one |
|
986 if (original.indexOf(".") >= 0) { |
|
987 var suffix = original.split(".").slice(-1)[0]; |
|
988 if (suffix && suffix.indexOf("_") < 0) |
|
989 processed += "." + suffix; |
|
990 } |
|
991 } |
|
992 return processed; |
|
993 } |
|
994 |
|
995 return aFileName.replace(re, "_"); |
|
996 } |
|
997 |
|
998 function getNormalizedLeafName(aFile, aDefaultExtension) |
|
999 { |
|
1000 if (!aDefaultExtension) |
|
1001 return aFile; |
|
1002 |
|
1003 #ifdef XP_WIN |
|
1004 // Remove trailing dots and spaces on windows |
|
1005 aFile = aFile.replace(/[\s.]+$/, ""); |
|
1006 #endif |
|
1007 |
|
1008 // Remove leading dots |
|
1009 aFile = aFile.replace(/^\.+/, ""); |
|
1010 |
|
1011 // Fix up the file name we're saving to to include the default extension |
|
1012 var i = aFile.lastIndexOf("."); |
|
1013 if (aFile.substr(i + 1) != aDefaultExtension) |
|
1014 return aFile + "." + aDefaultExtension; |
|
1015 |
|
1016 return aFile; |
|
1017 } |
|
1018 |
|
1019 function getDefaultExtension(aFilename, aURI, aContentType) |
|
1020 { |
|
1021 if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp") |
|
1022 return ""; // temporary fix for bug 120327 |
|
1023 |
|
1024 // First try the extension from the filename |
|
1025 const stdURLContractID = "@mozilla.org/network/standard-url;1"; |
|
1026 const stdURLIID = Components.interfaces.nsIURL; |
|
1027 var url = Components.classes[stdURLContractID].createInstance(stdURLIID); |
|
1028 url.filePath = aFilename; |
|
1029 |
|
1030 var ext = url.fileExtension; |
|
1031 |
|
1032 // This mirrors some code in nsExternalHelperAppService::DoContent |
|
1033 // Use the filename first and then the URI if that fails |
|
1034 |
|
1035 var mimeInfo = getMIMEInfoForType(aContentType, ext); |
|
1036 |
|
1037 if (ext && mimeInfo && mimeInfo.extensionExists(ext)) |
|
1038 return ext; |
|
1039 |
|
1040 // Well, that failed. Now try the extension from the URI |
|
1041 var urlext; |
|
1042 try { |
|
1043 url = aURI.QueryInterface(Components.interfaces.nsIURL); |
|
1044 urlext = url.fileExtension; |
|
1045 } catch (e) { |
|
1046 } |
|
1047 |
|
1048 if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { |
|
1049 return urlext; |
|
1050 } |
|
1051 else { |
|
1052 try { |
|
1053 if (mimeInfo) |
|
1054 return mimeInfo.primaryExtension; |
|
1055 } |
|
1056 catch (e) { |
|
1057 } |
|
1058 // Fall back on the extensions in the filename and URI for lack |
|
1059 // of anything better. |
|
1060 return ext || urlext; |
|
1061 } |
|
1062 } |
|
1063 |
|
1064 function GetSaveModeForContentType(aContentType, aDocument) |
|
1065 { |
|
1066 // We can only save a complete page if we have a loaded document |
|
1067 if (!aDocument) |
|
1068 return SAVEMODE_FILEONLY; |
|
1069 |
|
1070 // Find the possible save modes using the provided content type |
|
1071 var saveMode = SAVEMODE_FILEONLY; |
|
1072 switch (aContentType) { |
|
1073 case "text/html": |
|
1074 case "application/xhtml+xml": |
|
1075 case "image/svg+xml": |
|
1076 saveMode |= SAVEMODE_COMPLETE_TEXT; |
|
1077 // Fall through |
|
1078 case "text/xml": |
|
1079 case "application/xml": |
|
1080 saveMode |= SAVEMODE_COMPLETE_DOM; |
|
1081 break; |
|
1082 } |
|
1083 |
|
1084 return saveMode; |
|
1085 } |
|
1086 |
|
1087 function getCharsetforSave(aDocument) |
|
1088 { |
|
1089 if (aDocument) |
|
1090 return aDocument.characterSet; |
|
1091 |
|
1092 if (document.commandDispatcher.focusedWindow) |
|
1093 return document.commandDispatcher.focusedWindow.document.characterSet; |
|
1094 |
|
1095 return window.content.document.characterSet; |
|
1096 } |
|
1097 |
|
1098 /** |
|
1099 * Open a URL from chrome, determining if we can handle it internally or need to |
|
1100 * launch an external application to handle it. |
|
1101 * @param aURL The URL to be opened |
|
1102 */ |
|
1103 function openURL(aURL) |
|
1104 { |
|
1105 var uri = makeURI(aURL); |
|
1106 |
|
1107 var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] |
|
1108 .getService(Components.interfaces.nsIExternalProtocolService); |
|
1109 |
|
1110 if (!protocolSvc.isExposedProtocol(uri.scheme)) { |
|
1111 // If we're not a browser, use the external protocol service to load the URI. |
|
1112 protocolSvc.loadUrl(uri); |
|
1113 } |
|
1114 else { |
|
1115 var recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); |
|
1116 if (recentWindow) { |
|
1117 recentWindow.openUILinkIn(uri.spec, "tab"); |
|
1118 return; |
|
1119 } |
|
1120 |
|
1121 var loadgroup = Components.classes["@mozilla.org/network/load-group;1"] |
|
1122 .createInstance(Components.interfaces.nsILoadGroup); |
|
1123 var appstartup = Services.startup; |
|
1124 |
|
1125 var loadListener = { |
|
1126 onStartRequest: function ll_start(aRequest, aContext) { |
|
1127 appstartup.enterLastWindowClosingSurvivalArea(); |
|
1128 }, |
|
1129 onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) { |
|
1130 appstartup.exitLastWindowClosingSurvivalArea(); |
|
1131 }, |
|
1132 QueryInterface: function ll_QI(iid) { |
|
1133 if (iid.equals(Components.interfaces.nsISupports) || |
|
1134 iid.equals(Components.interfaces.nsIRequestObserver) || |
|
1135 iid.equals(Components.interfaces.nsISupportsWeakReference)) |
|
1136 return this; |
|
1137 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
1138 } |
|
1139 } |
|
1140 loadgroup.groupObserver = loadListener; |
|
1141 |
|
1142 var uriListener = { |
|
1143 onStartURIOpen: function(uri) { return false; }, |
|
1144 doContent: function(ctype, preferred, request, handler) { return false; }, |
|
1145 isPreferred: function(ctype, desired) { return false; }, |
|
1146 canHandleContent: function(ctype, preferred, desired) { return false; }, |
|
1147 loadCookie: null, |
|
1148 parentContentListener: null, |
|
1149 getInterface: function(iid) { |
|
1150 if (iid.equals(Components.interfaces.nsIURIContentListener)) |
|
1151 return this; |
|
1152 if (iid.equals(Components.interfaces.nsILoadGroup)) |
|
1153 return loadgroup; |
|
1154 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
1155 } |
|
1156 } |
|
1157 |
|
1158 var channel = Services.io.newChannelFromURI(uri); |
|
1159 var uriLoader = Components.classes["@mozilla.org/uriloader;1"] |
|
1160 .getService(Components.interfaces.nsIURILoader); |
|
1161 uriLoader.openURI(channel, |
|
1162 Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED, |
|
1163 uriListener); |
|
1164 } |
|
1165 } |