Bug 572417 - Release mouse capture in flash subclass after mouse events get delivered...
[mozilla-central.git] / toolkit / content / contentAreaUtils.js
blobfadbb5ef9ea0dc713142728a7449f444fc4d2fa7
1 # -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 
2 # ***** BEGIN LICENSE BLOCK *****
3 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License. You may obtain a copy of the License at
8 # http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
13 # License.
15 # The Original Code is mozilla.org code.
17 # The Initial Developer of the Original Code is
18 # Netscape Communications Corporation.
19 # Portions created by the Initial Developer are Copyright (C) 1998
20 # the Initial Developer. All Rights Reserved.
22 # Contributor(s):
23 #   Ben Goodger <ben@netscape.com> (Save File)
24 #   Fredrik Holmqvist <thesuckiestemail@yahoo.se>
25 #   Asaf Romano <mozilla.mano@sent.com>
26 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
27 #   Kathleen Brade <brade@pearlcrescent.com>
28 #   Mark Smith <mcs@pearlcrescent.com>
30 # Alternatively, the contents of this file may be used under the terms of
31 # either the GNU General Public License Version 2 or later (the "GPL"), or
32 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
33 # in which case the provisions of the GPL or the LGPL are applicable instead
34 # of those above. If you wish to allow use of your version of this file only
35 # under the terms of either the GPL or the LGPL, and not to allow others to
36 # use your version of this file under the terms of the MPL, indicate your
37 # decision by deleting the provisions above and replace them with the notice
38 # and other provisions required by the GPL or the LGPL. If you do not delete
39 # the provisions above, a recipient may use your version of this file under
40 # the terms of any one of the MPL, the GPL or the LGPL.
42 # ***** END LICENSE BLOCK *****
44 var ContentAreaUtils = {
45   get ioService() {
46     delete this.ioService;
47     return this.ioService =
48       Components.classes["@mozilla.org/network/io-service;1"]
49                 .getService(Components.interfaces.nsIIOService);
50   },
52   get stringBundle() {
53     delete this.stringBundle;
54     return this.stringBundle =
55       Components.classes["@mozilla.org/intl/stringbundle;1"]
56                 .getService(Components.interfaces.nsIStringBundleService)
57                 .createBundle("chrome://global/locale/contentAreaCommands.properties");
58   }
61 /**
62  * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal
63  * and checkLoadURIStrWithPrincipal.
64  * If |aPrincipal| is not allowed to link to |aURL|, this function throws with
65  * an error message.
66  *
67  * @param aURL
68  *        The URL a page has linked to. This could be passed either as a string
69  *        or as a nsIURI object.
70  * @param aPrincipal
71  *        The principal of the document from which aURL came.
72  * @param aFlags
73  *        Flags to be passed to checkLoadURIStr. If undefined,
74  *        nsIScriptSecurityManager.STANDARD will be passed.
75  */
76 function urlSecurityCheck(aURL, aPrincipal, aFlags)
78   const nsIScriptSecurityManager =
79     Components.interfaces.nsIScriptSecurityManager;
80   var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
81                          .getService(nsIScriptSecurityManager);
82   if (aFlags === undefined)
83     aFlags = nsIScriptSecurityManager.STANDARD;
85   try {
86     if (aURL instanceof Components.interfaces.nsIURI)
87       secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags);
88     else
89       secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags);
90   } catch (e) {
91     // XXXmano: dump the principal url here too
92     throw "Load of " + aURL + " denied.";
93   }
96 /**
97  * Determine whether or not a given focused DOMWindow is in the content area.
98  **/
99 function isContentFrame(aFocusedWindow)
101   if (!aFocusedWindow)
102     return false;
104   return (aFocusedWindow.top == window.content);
108 // Clientele: (Make sure you don't break any of these)
109 //  - File    ->  Save Page/Frame As...
110 //  - Context ->  Save Page/Frame As...
111 //  - Context ->  Save Link As...
112 //  - Alt-Click links in web pages
113 //  - Alt-Click links in the UI
115 // Try saving each of these types:
116 // - A complete webpage using File->Save Page As, and Context->Save Page As
117 // - A webpage as HTML only using the above methods
118 // - A webpage as Text only using the above methods
119 // - An image with an extension (e.g. .jpg) in its file name, using
120 //   Context->Save Image As...
121 // - An image without an extension (e.g. a banner ad on cnn.com) using
122 //   the above method.
123 // - A linked document using Save Link As...
124 // - A linked document using Alt-click Save Link As...
126 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
127                  aSkipPrompt, aReferrer)
129   internalSave(aURL, null, aFileName, null, null, aShouldBypassCache,
130                aFilePickerTitleKey, null, aReferrer, aSkipPrompt, null);
133 // Just like saveURL, but will get some info off the image before
134 // calling internalSave
135 // Clientele: (Make sure you don't break any of these)
136 //  - Context ->  Save Image As...
137 const imgICache = Components.interfaces.imgICache;
138 const nsISupportsCString = Components.interfaces.nsISupportsCString;
140 function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
141                       aSkipPrompt, aReferrer)
143   var contentType = null;
144   var contentDisposition = null;
145   if (!aShouldBypassCache) {
146     try {
147       var imageCache = Components.classes["@mozilla.org/image/cache;1"]
148                                  .getService(imgICache);
149       var props =
150         imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)));
151       if (props) {
152         contentType = props.get("type", nsISupportsCString);
153         contentDisposition = props.get("content-disposition",
154                                        nsISupportsCString);
155       }
156     } catch (e) {
157       // Failure to get type and content-disposition off the image is non-fatal
158     }
159   }
160   internalSave(aURL, null, aFileName, contentDisposition, contentType,
161                aShouldBypassCache, aFilePickerTitleKey, null, aReferrer,
162                aSkipPrompt, null);
165 function saveDocument(aDocument, aSkipPrompt)
167   if (!aDocument)
168     throw "Must have a document when calling saveDocument";
170   // We want to use cached data because the document is currently visible.
171   var ifreq =
172     aDocument.defaultView
173              .QueryInterface(Components.interfaces.nsIInterfaceRequestor);
175   var contentDisposition = null;
176   try {
177     contentDisposition =
178       ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils)
179            .getDocumentMetadata("content-disposition");
180   } catch (ex) {
181     // Failure to get a content-disposition is ok
182   }
184   var cacheKey = null;
185   try {
186     cacheKey =
187       ifreq.getInterface(Components.interfaces.nsIWebNavigation)
188            .QueryInterface(Components.interfaces.nsIWebPageDescriptor);
189   } catch (ex) {
190     // We might not find it in the cache.  Oh, well.
191   }
193   internalSave(aDocument.location.href, aDocument, null, contentDisposition,
194                aDocument.contentType, false, null, null,
195                aDocument.referrer ? makeURI(aDocument.referrer) : null,
196                aSkipPrompt, cacheKey);
199 function DownloadListener(win, transfer) {
200   function makeClosure(name) {
201     return function() {
202       transfer[name].apply(transfer, arguments);
203     }
204   }
206   this.window = win;
208   // Now... we need to forward all calls to our transfer
209   for (var i in transfer) {
210     if (i != "QueryInterface")
211       this[i] = makeClosure(i);
212   }
215 DownloadListener.prototype = {
216   QueryInterface: function dl_qi(aIID)
217   {
218     if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
219         aIID.equals(Components.interfaces.nsIWebProgressListener) ||
220         aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
221         aIID.equals(Components.interfaces.nsISupports)) {
222       return this;
223     }
224     throw Components.results.NS_ERROR_NO_INTERFACE;
225   },
227   getInterface: function dl_gi(aIID)
228   {
229     if (aIID.equals(Components.interfaces.nsIAuthPrompt) ||
230         aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
231       var ww =
232         Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
233                   .getService(Components.interfaces.nsIPromptFactory);
234       return ww.getPrompt(this.window, aIID);
235     }
237     throw Components.results.NS_ERROR_NO_INTERFACE;
238   }
241 const kSaveAsType_Complete = 0; // Save document with attached objects.
242 // const kSaveAsType_URL      = 1; // Save document or URL by itself.
243 const kSaveAsType_Text     = 2; // Save document, converting to plain text.
246  * internalSave: Used when saving a document or URL.
248  * If aChosenData is null, this method:
249  *  - Determines a local target filename to use
250  *  - Prompts the user to confirm the destination filename and save mode
251  *    (aContentType affects this)
252  *  - [Note] This process involves the parameters aURL, aReferrer (to determine
253  *    how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey,
254  *    and aSkipPrompt.
256  * If aChosenData is non-null, this method:
257  *  - Uses the provided source URI and save file name
258  *  - Saves the document as complete DOM if possible (aDocument present and
259  *    right aContentType)
260  *  - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
261  *    aSkipPrompt are ignored.
263  * In any case, this method:
264  *  - Creates a 'Persist' object (which will perform the saving in the
265  *    background) and then starts it.
266  *  - [Note] This part of the process only involves the parameters aDocument,
267  *    aShouldBypassCache and aReferrer. The source, the save name and the save
268  *    mode are the ones determined previously.
270  * @param aURL
271  *        The String representation of the URL of the document being saved
272  * @param aDocument
273  *        The document to be saved
274  * @param aDefaultFileName
275  *        The caller-provided suggested filename if we don't 
276  *        find a better one
277  * @param aContentDisposition
278  *        The caller-provided content-disposition header to use.
279  * @param aContentType
280  *        The caller-provided content-type to use
281  * @param aShouldBypassCache
282  *        If true, the document will always be refetched from the server
283  * @param aFilePickerTitleKey
284  *        Alternate title for the file picker
285  * @param aChosenData
286  *        If non-null this contains an instance of object AutoChosen (see below)
287  *        which holds pre-determined data so that the user does not need to be
288  *        prompted for a target filename.
289  * @param aReferrer
290  *        the referrer URI object (not URL string) to use, or null
291  *        if no referrer should be sent.
292  * @param aSkipPrompt [optional]
293  *        If set to true, we will attempt to save the file to the
294  *        default downloads folder without prompting.
295  * @param aCacheKey [optional]
296  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
297  *        allowed values.
298  */
299 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
300                       aContentType, aShouldBypassCache, aFilePickerTitleKey,
301                       aChosenData, aReferrer, aSkipPrompt, aCacheKey)
303   if (aSkipPrompt == undefined)
304     aSkipPrompt = false;
306   if (aCacheKey == undefined)
307     aCacheKey = null;
309   // Note: aDocument == null when this code is used by save-link-as...
310   var saveMode = GetSaveModeForContentType(aContentType, aDocument);
312   var file, sourceURI, saveAsType;
313   // Find the URI object for aURL and the FileName/Extension to use when saving.
314   // FileName/Extension will be ignored if aChosenData supplied.
315   if (aChosenData) {
316     file = aChosenData.file;
317     sourceURI = aChosenData.uri;
318     saveAsType = kSaveAsType_Complete;
319   } else {
320     var charset = null;
321     if (aDocument)
322       charset = aDocument.characterSet;
323     else if (aReferrer)
324       charset = aReferrer.originCharset;
325     var fileInfo = new FileInfo(aDefaultFileName);
326     initFileInfo(fileInfo, aURL, charset, aDocument,
327                  aContentType, aContentDisposition);
328     sourceURI = fileInfo.uri;
330     var fpParams = {
331       fpTitleKey: aFilePickerTitleKey,
332       fileInfo: fileInfo,
333       contentType: aContentType,
334       saveMode: saveMode,
335       saveAsType: kSaveAsType_Complete,
336       file: file
337     };
339     if (!getTargetFile(fpParams, aSkipPrompt))
340       // If the method returned false this is because the user cancelled from
341       // the save file picker dialog.
342       return;
344     saveAsType = fpParams.saveAsType;
345     file = fpParams.file;
346   }
348   // XXX We depend on the following holding true in appendFiltersForContentType():
349   // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
350   // If we should save as text, the saveAsType is kSaveAsType_Text.
351   var useSaveDocument = aDocument &&
352                         (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) ||
353                          ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text)));
354   // If we're saving a document, and are saving either in complete mode or
355   // as converted text, pass the document to the web browser persist component.
356   // If we're just saving the HTML (second option in the list), send only the URI.
357   var persistArgs = {
358     sourceURI         : sourceURI,
359     sourceReferrer    : aReferrer,
360     sourceDocument    : useSaveDocument ? aDocument : null,
361     targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null,
362     targetFile        : file,
363     sourceCacheKey    : aCacheKey,
364     sourcePostData    : aDocument ? getPostData(aDocument) : null,
365     bypassCache       : aShouldBypassCache
366   };
368   // Start the actual save process
369   internalPersist(persistArgs);
373  * internalPersist: Creates a 'Persist' object (which will perform the saving
374  *  in the background) and then starts it.
376  * @param persistArgs.sourceURI
377  *        The nsIURI of the document being saved
378  * @param persistArgs.sourceCacheKey [optional]
379  *        If set will be passed to saveURI
380  * @param persistArgs.sourceDocument [optional]
381  *        The document to be saved, or null if not saving a complete document
382  * @param persistArgs.sourceReferrer
383  *        Required and used only when persistArgs.sourceDocument is NOT present,
384  *        the nsIURI of the referrer to use, or null if no referrer should be
385  *        sent.
386  * @param persistArgs.sourcePostData
387  *        Required and used only when persistArgs.sourceDocument is NOT present,
388  *        represents the POST data to be sent along with the HTTP request, and
389  *        must be null if no POST data should be sent.
390  * @param persistArgs.targetFile
391  *        The nsIFile of the file to create
392  * @param persistArgs.targetContentType
393  *        Required and used only when persistArgs.sourceDocument is present,
394  *        determines the final content type of the saved file, or null to use
395  *        the same content type as the source document. Currently only
396  *        "text/plain" is meaningful.
397  * @param persistArgs.bypassCache
398  *        If true, the document will always be refetched from the server
399  */
400 function internalPersist(persistArgs)
402   var persist = makeWebBrowserPersist();
404   // Calculate persist flags.
405   const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
406   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
407                 nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES;
408   if (persistArgs.bypassCache)
409     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
410   else
411     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
413   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
414   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
416   // Find the URI associated with the target file
417   var targetFileURL = makeFileURI(persistArgs.targetFile);
419   // Create download and initiate it (below)
420   var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer);
421   tr.init(persistArgs.sourceURI,
422           targetFileURL, "", null, null, null, persist);
423   persist.progressListener = new DownloadListener(window, tr);
425   if (persistArgs.sourceDocument) {
426     // Saving a Document, not a URI:
427     var filesFolder = null;
428     if (persistArgs.targetContentType != "text/plain") {
429       // Create the local directory into which to save associated files.
430       filesFolder = persistArgs.targetFile.clone();
432       var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
433       var filesFolderLeafName =
434         ContentAreaUtils.stringBundle
435                         .formatStringFromName("filesFolder", [nameWithoutExtension], 1);
437       filesFolder.leafName = filesFolderLeafName;
438     }
440     var encodingFlags = 0;
441     if (persistArgs.targetContentType == "text/plain") {
442       encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
443       encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
444       encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
445     }
446     else {
447       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
448     }
450     const kWrapColumn = 80;
451     persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder,
452                          persistArgs.targetContentType, encodingFlags, kWrapColumn);
453   } else {
454     persist.saveURI(persistArgs.sourceURI,
455                     persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null,
456                     targetFileURL);
457   }
461  * Structure for holding info about automatically supplied parameters for
462  * internalSave(...). This allows parameters to be supplied so the user does not
463  * need to be prompted for file info.
464  * @param aFileAutoChosen This is an nsILocalFile object that has been
465  *        pre-determined as the filename for the target to save to
466  * @param aUriAutoChosen  This is the nsIURI object for the target
467  */
468 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
469   this.file = aFileAutoChosen;
470   this.uri  = aUriAutoChosen;
474  * Structure for holding info about a URL and the target filename it should be
475  * saved to. This structure is populated by initFileInfo(...).
476  * @param aSuggestedFileName This is used by initFileInfo(...) when it
477  *        cannot 'discover' the filename from the url 
478  * @param aFileName The target filename
479  * @param aFileBaseName The filename without the file extension
480  * @param aFileExt The extension of the filename
481  * @param aUri An nsIURI object for the url that is being saved
482  */
483 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) {
484   this.suggestedFileName = aSuggestedFileName;
485   this.fileName = aFileName;
486   this.fileBaseName = aFileBaseName;
487   this.fileExt = aFileExt;
488   this.uri = aUri;
492  * Determine what the 'default' filename string is, its file extension and the
493  * filename without the extension. This filename is used when prompting the user
494  * for confirmation in the file picker dialog.
495  * @param aFI A FileInfo structure into which we'll put the results of this method.
496  * @param aURL The String representation of the URL of the document being saved
497  * @param aURLCharset The charset of aURL.
498  * @param aDocument The document to be saved
499  * @param aContentType The content type we're saving, if it could be
500  *        determined by the caller.
501  * @param aContentDisposition The content-disposition header for the object
502  *        we're saving, if it could be determined by the caller.
503  */
504 function initFileInfo(aFI, aURL, aURLCharset, aDocument,
505                       aContentType, aContentDisposition)
507   try {
508     // Get an nsIURI object from aURL if possible:
509     try {
510       aFI.uri = makeURI(aURL, aURLCharset);
511       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
512       // populate extra object fields (eg filename and file extension).
513       var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL);
514       aFI.fileExt = url.fileExtension;
515     } catch (e) {
516     }
518     // Get the default filename:
519     aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName),
520                                       aFI.uri, aDocument, aContentDisposition);
521     // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
522     // if saveURL(...) was the original caller (hence both aContentType and
523     // aDocument are blank). If they were saving a link to a website then make
524     // the extension .htm .
525     if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) {
526       aFI.fileExt = "htm";
527       aFI.fileBaseName = aFI.fileName;
528     } else {
529       aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
530       aFI.fileBaseName = getFileBaseName(aFI.fileName);
531     }
532   } catch (e) {
533   }
536 /** 
537  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
538  * prompting the user to confirm (or change) the fileName.
539  * @param aFpP
540  *        A structure (see definition in internalSave(...) method)
541  *        containing all the data used within this method.
542  * @param aSkipPrompt
543  *        If true, attempt to save the file automatically to the user's default
544  *        download directory, thus skipping the explicit prompt for a file name,
545  *        but only if the associated preference is set.
546  *        If false, don't save the file automatically to the user's
547  *        default download directory, even if the associated preference
548  *        is set, but ask for the target explicitly.
549  * @return true if the user confirmed a filename in the picker or the picker
550  *         was not displayed; false if they dismissed the picker.
551  */
552 function getTargetFile(aFpP, /* optional */ aSkipPrompt)
554   if (typeof gDownloadLastDir != "object")
555     Components.utils.import("resource://gre/modules/DownloadLastDir.jsm");
557   var prefs = getPrefsBrowserDownload("browser.download.");
558   var useDownloadDir = prefs.getBoolPref("useDownloadDir");
559   const nsILocalFile = Components.interfaces.nsILocalFile;
561   if (!aSkipPrompt)
562     useDownloadDir = false;
564   // Default to the user's default downloads directory configured
565   // through download prefs.
566   var dlMgr = Components.classes["@mozilla.org/download-manager;1"]
567                         .getService(Components.interfaces.nsIDownloadManager);
568   var dir = dlMgr.userDownloadsDirectory;
569   var dirExists = dir && dir.exists();
571   if (useDownloadDir && dirExists) {
572     dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName,
573                                      aFpP.fileInfo.fileExt));
574     aFpP.file = uniqueFile(dir);
575     return true;
576   }
578   // We must prompt for the file name explicitly.
579   // If we must prompt because we were asked to...
580   if (!useDownloadDir) try {
581     // ...find the directory that was last used for saving, and use it in the
582     // file picker if it is still valid. Otherwise, keep the default of the
583     // user's default downloads directory. If it doesn't exist, it will be
584     // changed to the user's desktop later.
585     var lastDir = gDownloadLastDir.file;
586     if (lastDir.exists()) {
587       dir = lastDir;
588       dirExists = true;
589     }
590   } catch(e) {}
592   if (!dirExists) {
593     // Default to desktop.
594     var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
595                                 .getService(Components.interfaces.nsIProperties);
596     dir = fileLocator.get("Desk", nsILocalFile);
597   }
599   var fp = makeFilePicker();
600   var titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
601   fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
602           Components.interfaces.nsIFilePicker.modeSave);
604   fp.displayDirectory = dir;
605   fp.defaultExtension = aFpP.fileInfo.fileExt;
606   fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName,
607                                            aFpP.fileInfo.fileExt);
608   appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt,
609                               aFpP.saveMode);
611   // The index of the selected filter is only preserved and restored if there's
612   // more than one filter in addition to "All Files".
613   if (aFpP.saveMode != SAVEMODE_FILEONLY) {
614     try {
615       fp.filterIndex = prefs.getIntPref("save_converter_index");
616     }
617     catch (e) {
618     }
619   }
621   if (fp.show() == Components.interfaces.nsIFilePicker.returnCancel || !fp.file)
622     return false;
624   if (aFpP.saveMode != SAVEMODE_FILEONLY)
625     prefs.setIntPref("save_converter_index", fp.filterIndex);
627   // Do not store the last save directory as a pref inside the private browsing mode
628   var directory = fp.file.parent.QueryInterface(nsILocalFile);
629   gDownloadLastDir.file = directory;
631   fp.file.leafName = validateFileName(fp.file.leafName);
632   
633   aFpP.saveAsType = fp.filterIndex;
634   aFpP.file = fp.file;
635   aFpP.fileURL = fp.fileURL;
636   return true;
639 // Since we're automatically downloading, we don't get the file picker's
640 // logic to check for existing files, so we need to do that here.
642 // Note - this code is identical to that in
643 //   mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
644 // If you are updating this code, update that code too! We can't share code
645 // here since that code is called in a js component.
646 function uniqueFile(aLocalFile)
648   var collisionCount = 0;
649   while (aLocalFile.exists()) {
650     collisionCount++;
651     if (collisionCount == 1) {
652       // Append "(2)" before the last dot in (or at the end of) the filename
653       // special case .ext.gz etc files so we don't wind up with .tar(2).gz
654       if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
655         aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
656       else
657         aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
658     }
659     else {
660       // replace the last (n) in the filename with (n+1)
661       aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")");
662     }
663   }
664   return aLocalFile;
667 // We have no DOM, and can only save the URL as is.
668 const SAVEMODE_FILEONLY      = 0x00;
669 // We have a DOM and can save as complete.
670 const SAVEMODE_COMPLETE_DOM  = 0x01;
671 // We have a DOM which we can serialize as text.
672 const SAVEMODE_COMPLETE_TEXT = 0x02;
674 // If we are able to save a complete DOM, the 'save as complete' filter
675 // must be the first filter appended.  The 'save page only' counterpart
676 // must be the second filter appended.  And the 'save as complete text'
677 // filter must be the third filter appended.
678 function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode)
680   // The bundle name for saving only a specific content type.
681   var bundleName;
682   // The corresponding filter string for a specific content type.
683   var filterString;
685   // XXX all the cases that are handled explicitly here MUST be handled
686   // in GetSaveModeForContentType to return a non-fileonly filter.
687   switch (aContentType) {
688   case "text/html":
689     bundleName   = "WebPageHTMLOnlyFilter";
690     filterString = "*.htm; *.html";
691     break;
693   case "application/xhtml+xml":
694     bundleName   = "WebPageXHTMLOnlyFilter";
695     filterString = "*.xht; *.xhtml";
696     break;
698   case "image/svg+xml":
699     bundleName   = "WebPageSVGOnlyFilter";
700     filterString = "*.svg; *.svgz";
701     break;
703   case "text/xml":
704   case "application/xml":
705     bundleName   = "WebPageXMLOnlyFilter";
706     filterString = "*.xml";
707     break;
709   default:
710     if (aSaveMode != SAVEMODE_FILEONLY)
711       throw "Invalid save mode for type '" + aContentType + "'";
713     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
714     if (mimeInfo) {
716       var extEnumerator = mimeInfo.getFileExtensions();
718       var extString = "";
719       while (extEnumerator.hasMore()) {
720         var extension = extEnumerator.getNext();
721         if (extString)
722           extString += "; ";    // If adding more than one extension,
723                                 // separate by semi-colon
724         extString += "*." + extension;
725       }
727       if (extString)
728         aFilePicker.appendFilter(mimeInfo.description, extString);
729     }
731     break;
732   }
734   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
735     aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
736                              filterString);
737     // We should always offer a choice to save document only if
738     // we allow saving as complete.
739     aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
740                              filterString);
741   }
743   if (aSaveMode & SAVEMODE_COMPLETE_TEXT)
744     aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText);
746   // Always append the all files (*) filter
747   aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
750 function getPostData(aDocument)
752   try {
753     // Find the session history entry corresponding to the given document. In
754     // the current implementation, nsIWebPageDescriptor.currentDescriptor always
755     // returns a session history entry.
756     var sessionHistoryEntry =
757         aDocument.defaultView
758                  .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
759                  .getInterface(Components.interfaces.nsIWebNavigation)
760                  .QueryInterface(Components.interfaces.nsIWebPageDescriptor)
761                  .currentDescriptor
762                  .QueryInterface(Components.interfaces.nsISHEntry);
763     return sessionHistoryEntry.postData;
764   }
765   catch (e) {
766   }
767   return null;
770 // Get the preferences branch ("browser.download." for normal 'save' mode)...
771 function getPrefsBrowserDownload(branch)
773   const prefSvcContractID = "@mozilla.org/preferences-service;1";
774   const prefSvcIID = Components.interfaces.nsIPrefService;                              
775   return Components.classes[prefSvcContractID].getService(prefSvcIID).getBranch(branch);
778 function makeWebBrowserPersist()
780   const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
781   const persistIID = Components.interfaces.nsIWebBrowserPersist;
782   return Components.classes[persistContractID].createInstance(persistIID);
786  * Constructs a new URI, using nsIIOService.
787  * @param aURL The URI spec.
788  * @param aOriginCharset The charset of the URI.
789  * @param aBaseURI Base URI to resolve aURL, or null.
790  * @return an nsIURI object based on aURL.
791  */
792 function makeURI(aURL, aOriginCharset, aBaseURI)
794   return ContentAreaUtils.ioService.newURI(aURL, aOriginCharset, aBaseURI);
797 function makeFileURI(aFile)
799   return ContentAreaUtils.ioService.newFileURI(aFile);
802 function makeFilePicker()
804   const fpContractID = "@mozilla.org/filepicker;1";
805   const fpIID = Components.interfaces.nsIFilePicker;
806   return Components.classes[fpContractID].createInstance(fpIID);
809 function getMIMEService()
811   const mimeSvcContractID = "@mozilla.org/mime;1";
812   const mimeSvcIID = Components.interfaces.nsIMIMEService;
813   const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID);
814   return mimeSvc;
817 // Given aFileName, find the fileName without the extension on the end.
818 function getFileBaseName(aFileName)
820   // Remove the file extension from aFileName:
821   return aFileName.replace(/\.[^.]*$/, "");
824 function getMIMETypeForURI(aURI)
826   try {
827     return getMIMEService().getTypeFromURI(aURI);
828   }
829   catch (e) {
830   }
831   return null;
834 function getMIMEInfoForType(aMIMEType, aExtension)
836   if (aMIMEType || aExtension) {
837     try {
838       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
839     }
840     catch (e) {
841     }
842   }
843   return null;
846 function getDefaultFileName(aDefaultFileName, aURI, aDocument,
847                             aContentDisposition)
849   // 1) look for a filename in the content-disposition header, if any
850   if (aContentDisposition) {
851     const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
852     const mhpIID = Components.interfaces.nsIMIMEHeaderParam;
853     const mhp = Components.classes[mhpContractID].getService(mhpIID);
854     var dummy = { value: null };  // Need an out param...
855     var charset = getCharsetforSave(aDocument);
857     var fileName = null;
858     try {
859       fileName = mhp.getParameter(aContentDisposition, "filename", charset,
860                                   true, dummy);
861     }
862     catch (e) {
863       try {
864         fileName = mhp.getParameter(aContentDisposition, "name", charset, true,
865                                     dummy);
866       }
867       catch (e) {
868       }
869     }
870     if (fileName)
871       return fileName;
872   }
874   try {
875     var url = aURI.QueryInterface(Components.interfaces.nsIURL);
876     if (url.fileName != "") {
877       // 2) Use the actual file name, if present
878       var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
879                                    .getService(Components.interfaces.nsITextToSubURI);
880       return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName));
881     }
882   } catch (e) {
883     // This is something like a data: and so forth URI... no filename here.
884   }
886   if (aDocument) {
887     var docTitle = validateFileName(aDocument.title).replace(/^\s+|\s+$/g, "");
888     if (docTitle) {
889       // 3) Use the document title
890       return docTitle;
891     }
892   }
894   if (aDefaultFileName)
895     // 4) Use the caller-provided name, if any
896     return validateFileName(aDefaultFileName);
898   // 5) If this is a directory, use the last directory name
899   var path = aURI.path.match(/\/([^\/]+)\/$/);
900   if (path && path.length > 1)
901     return validateFileName(path[1]);
903   try {
904     if (aURI.host)
905       // 6) Use the host.
906       return aURI.host;
907   } catch (e) {
908     // Some files have no information at all, like Javascript generated pages
909   }
910   try {
911     // 7) Use the default file name
912     return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName");
913   } catch (e) {
914     //in case localized string cannot be found
915   }
916   // 8) If all else fails, use "index"
917   return "index";
920 function validateFileName(aFileName)
922   var re = /[\/]+/g;
923   if (navigator.appVersion.indexOf("Windows") != -1) {
924     re = /[\\\/\|]+/g;
925     aFileName = aFileName.replace(/[\"]+/g, "'");
926     aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
927     aFileName = aFileName.replace(/[\<]+/g, "(");
928     aFileName = aFileName.replace(/[\>]+/g, ")");
929   }
930   else if (navigator.appVersion.indexOf("Macintosh") != -1)
931     re = /[\:\/]+/g;
932   
933   return aFileName.replace(re, "_");
936 function getNormalizedLeafName(aFile, aDefaultExtension)
938   if (!aDefaultExtension)
939     return aFile;
941 #ifdef XP_WIN
942   // Remove trailing dots and spaces on windows
943   aFile = aFile.replace(/[\s.]+$/, "");
944 #endif
946   // Remove leading dots
947   aFile = aFile.replace(/^\.+/, "");
949   // Fix up the file name we're saving to to include the default extension
950   var i = aFile.lastIndexOf(".");
951   if (aFile.substr(i + 1) != aDefaultExtension)
952     return aFile + "." + aDefaultExtension;
954   return aFile;
957 function getDefaultExtension(aFilename, aURI, aContentType)
959   if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp")
960     return "";   // temporary fix for bug 120327
962   // First try the extension from the filename
963   const stdURLContractID = "@mozilla.org/network/standard-url;1";
964   const stdURLIID = Components.interfaces.nsIURL;
965   var url = Components.classes[stdURLContractID].createInstance(stdURLIID);
966   url.filePath = aFilename;
968   var ext = url.fileExtension;
970   // This mirrors some code in nsExternalHelperAppService::DoContent
971   // Use the filename first and then the URI if that fails
973   var mimeInfo = getMIMEInfoForType(aContentType, ext);
975   if (ext && mimeInfo && mimeInfo.extensionExists(ext))
976     return ext;
978   // Well, that failed.  Now try the extension from the URI
979   var urlext;
980   try {
981     url = aURI.QueryInterface(Components.interfaces.nsIURL);
982     urlext = url.fileExtension;
983   } catch (e) {
984   }
986   if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
987     return urlext;
988   }
989   else {
990     try {
991       if (mimeInfo)
992         return mimeInfo.primaryExtension;
993     }
994     catch (e) {
995     }
996     // Fall back on the extensions in the filename and URI for lack
997     // of anything better.
998     return ext || urlext;
999   }
1002 function GetSaveModeForContentType(aContentType, aDocument)
1004   // We can only save a complete page if we have a loaded document
1005   if (!aDocument)
1006     return SAVEMODE_FILEONLY;
1008   // Find the possible save modes using the provided content type
1009   var saveMode = SAVEMODE_FILEONLY;
1010   switch (aContentType) {
1011   case "text/html":
1012   case "application/xhtml+xml":
1013   case "image/svg+xml":
1014     saveMode |= SAVEMODE_COMPLETE_TEXT;
1015     // Fall through
1016   case "text/xml":
1017   case "application/xml":
1018     saveMode |= SAVEMODE_COMPLETE_DOM;
1019     break;
1020   }
1022   return saveMode;
1025 function getCharsetforSave(aDocument)
1027   if (aDocument)
1028     return aDocument.characterSet;
1030   if (document.commandDispatcher.focusedWindow)
1031     return document.commandDispatcher.focusedWindow.document.characterSet;
1033   return window.content.document.characterSet;
1037  * Open a URL from chrome, determining if we can handle it internally or need to
1038  *  launch an external application to handle it.
1039  * @param aURL The URL to be opened
1040  */
1041 function openURL(aURL)
1043   var uri = makeURI(aURL);
1045   var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
1046                               .getService(Components.interfaces.nsIExternalProtocolService);
1048   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
1049     // If we're not a browser, use the external protocol service to load the URI.
1050     protocolSvc.loadUrl(uri);
1051   }
1052   else {
1053     var loadgroup = Components.classes["@mozilla.org/network/load-group;1"]
1054                               .createInstance(Components.interfaces.nsILoadGroup);
1055     var appstartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
1056                                .getService(Components.interfaces.nsIAppStartup);
1058     var loadListener = {
1059       onStartRequest: function ll_start(aRequest, aContext) {
1060         appstartup.enterLastWindowClosingSurvivalArea();
1061       },
1062       onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) {
1063         appstartup.exitLastWindowClosingSurvivalArea();
1064       },
1065       QueryInterface: function ll_QI(iid) {
1066         if (iid.equals(Components.interfaces.nsISupports) ||
1067             iid.equals(Components.interfaces.nsIRequestObserver) ||
1068             iid.equals(Components.interfaces.nsISupportsWeakReference))
1069           return this;
1070         throw Components.results.NS_ERROR_NO_INTERFACE;
1071       }
1072     }
1073     loadgroup.groupObserver = loadListener;
1075     var uriListener = {
1076       onStartURIOpen: function(uri) { return false; },
1077       doContent: function(ctype, preferred, request, handler) { return false; },
1078       isPreferred: function(ctype, desired) { return false; },
1079       canHandleContent: function(ctype, preferred, desired) { return false; },
1080       loadCookie: null,
1081       parentContentListener: null,
1082       getInterface: function(iid) {
1083         if (iid.equals(Components.interfaces.nsIURIContentListener))
1084           return this;
1085         if (iid.equals(Components.interfaces.nsILoadGroup))
1086           return loadgroup;
1087         throw Components.results.NS_ERROR_NO_INTERFACE;
1088       }
1089     }
1091     var channel = ContentAreaUtils.ioService.newChannelFromURI(uri);
1092     var uriLoader = Components.classes["@mozilla.org/uriloader;1"]
1093                               .getService(Components.interfaces.nsIURILoader);
1094     uriLoader.openURI(channel, true, uriListener);
1095   }