Bug 84106. Actually pass a cache key to web browser persist so that POST result...
[mozilla-central.git] / toolkit / content / contentAreaUtils.js
blob4a1058377e9d8b36b0a2a94351948b9c81933305
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>
28 # Alternatively, the contents of this file may be used under the terms of
29 # either the GNU General Public License Version 2 or later (the "GPL"), or
30 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
31 # in which case the provisions of the GPL or the LGPL are applicable instead
32 # of those above. If you wish to allow use of your version of this file only
33 # under the terms of either the GPL or the LGPL, and not to allow others to
34 # use your version of this file under the terms of the MPL, indicate your
35 # decision by deleting the provisions above and replace them with the notice
36 # and other provisions required by the GPL or the LGPL. If you do not delete
37 # the provisions above, a recipient may use your version of this file under
38 # the terms of any one of the MPL, the GPL or the LGPL.
40 # ***** END LICENSE BLOCK *****
42 /**
43  * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal
44  * and checkLoadURIStrWithPrincipal.
45  * If |aPrincipal| is not allowed to link to |aURL|, this function throws with
46  * an error message.
47  *
48  * @param aURL
49  *        The URL a page has linked to. This could be passed either as a string
50  *        or as a nsIURI object.
51  * @param aPrincipal
52  *        The principal of the document from which aURL came.
53  * @param aFlags
54  *        Flags to be passed to checkLoadURIStr. If undefined,
55  *        nsIScriptSecurityManager.STANDARD will be passed.
56  */
57 function urlSecurityCheck(aURL, aPrincipal, aFlags)
59   const nsIScriptSecurityManager =
60     Components.interfaces.nsIScriptSecurityManager;
61   var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
62                          .getService(nsIScriptSecurityManager);
63   if (aFlags === undefined)
64     aFlags = nsIScriptSecurityManager.STANDARD;
66   try {
67     if (aURL instanceof Components.interfaces.nsIURI)
68       secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags);
69     else
70       secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags);
71   } catch (e) {
72     // XXXmano: dump the principal url here too
73     throw "Load of " + aURL + " denied.";
74   }
77 /**
78  * Determine whether or not a given focused DOMWindow is in the content area.
79  **/
80 function isContentFrame(aFocusedWindow)
82   if (!aFocusedWindow)
83     return false;
85   return (aFocusedWindow.top == window.content);
89 // Clientelle: (Make sure you don't break any of these)
90 //  - File    ->  Save Page/Frame As...
91 //  - Context ->  Save Page/Frame As...
92 //  - Context ->  Save Link As...
93 //  - Alt-Click links in web pages
94 //  - Alt-Click links in the UI
96 // Try saving each of these types:
97 // - A complete webpage using File->Save Page As, and Context->Save Page As
98 // - A webpage as HTML only using the above methods
99 // - A webpage as Text only using the above methods
100 // - An image with an extension (e.g. .jpg) in its file name, using
101 //   Context->Save Image As...
102 // - An image without an extension (e.g. a banner ad on cnn.com) using
103 //   the above method.
104 // - A linked document using Save Link As...
105 // - A linked document using Alt-click Save Link As...
107 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
108                  aSkipPrompt, aReferrer)
110   internalSave(aURL, null, aFileName, null, null, aShouldBypassCache,
111                aFilePickerTitleKey, null, aReferrer, aSkipPrompt, null);
114 // Just like saveURL, but will get some info off the image before
115 // calling internalSave
116 // Clientelle: (Make sure you don't break any of these)
117 //  - Context ->  Save Image As...
118 const imgICache = Components.interfaces.imgICache;
119 const nsISupportsCString = Components.interfaces.nsISupportsCString;
121 function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
122                       aSkipPrompt, aReferrer)
124   var contentType = null;
125   var contentDisposition = null;
126   if (!aShouldBypassCache) {
127     try {
128       var imageCache = Components.classes["@mozilla.org/image/cache;1"]
129                                  .getService(imgICache);
130       var props =
131         imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)));
132       if (props) {
133         contentType = props.get("type", nsISupportsCString);
134         contentDisposition = props.get("content-disposition",
135                                        nsISupportsCString);
136       }
137     } catch (e) {
138       // Failure to get type and content-disposition off the image is non-fatal
139     }
140   }
141   internalSave(aURL, null, aFileName, contentDisposition, contentType,
142                aShouldBypassCache, aFilePickerTitleKey, null, aReferrer,
143                aSkipPrompt, null);
146 function saveFrameDocument()
148   var focusedWindow = document.commandDispatcher.focusedWindow;
149   if (isContentFrame(focusedWindow))
150     saveDocument(focusedWindow.document);
153 function saveDocument(aDocument, aSkipPrompt)
155   if (!aDocument)
156     throw "Must have a document when calling saveDocument";
158   // We want to use cached data because the document is currently visible.
159   var ifreq =
160     aDocument.defaultView
161              .QueryInterface(Components.interfaces.nsIInterfaceRequestor);
163   var contentDisposition = null;
164   try {
165     contentDisposition =
166       ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils)
167            getDocumentMetadata("content-disposition");
168   } catch (ex) {
169     // Failure to get a content-disposition is ok
170   }
172   var cacheKey = null;
173   try {
174     cacheKey =
175       ifreq.getInterface(Components.interfaces.nsIWebNavigation)
176            .QueryInterface(Components.interfaces.nsIWebPageDescriptor);
177   } catch (ex) {
178     // We might not find it in the cache.  Oh, well.
179   }
181   internalSave(aDocument.location.href, aDocument, null, contentDisposition,
182                aDocument.contentType, false, null, null,
183                aDocument.referrer ? makeURI(aDocument.referrer) : null,
184                aSkipPrompt, cacheKey);
187 function DownloadListener(win, transfer) {
188   function makeClosure(name) {
189     return function() {
190       transfer[name].apply(transfer, arguments);
191     }
192   }
194   this.window = win;
196   // Now... we need to forward all calls to our transfer
197   for (var i in transfer) {
198     if (i != "QueryInterface")
199       this[i] = makeClosure(i);
200   }
203 DownloadListener.prototype = {
204   QueryInterface: function dl_qi(aIID)
205   {
206     if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
207         aIID.equals(Components.interfaces.nsIWebProgressListener) ||
208         aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
209         aIID.equals(Components.interfaces.nsISupports)) {
210       return this;
211     }
212     throw Components.results.NS_ERROR_NO_INTERFACE;
213   },
215   getInterface: function dl_gi(aIID)
216   {
217     if (aIID.equals(Components.interfaces.nsIAuthPrompt) ||
218         aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
219       var ww =
220         Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
221                   .getService(Components.interfaces.nsIPromptFactory);
222       return ww.getPrompt(this.window, aIID);
223     }
225     throw Components.results.NS_ERROR_NO_INTERFACE;
226   }
229 const kSaveAsType_Complete = 0; // Save document with attached objects.
230 // const kSaveAsType_URL      = 1; // Save document or URL by itself.
231 const kSaveAsType_Text     = 2; // Save document, converting to plain text.
234  * internalSave: Used when saving a document or URL. This method:
235  *  - Determines a local target filename to use (unless parameter
236  *    aChosenData is non-null)
237  *  - Determines content-type if possible
238  *  - Prompts the user to confirm the destination filename and save mode
239  *    (content-type affects this)
240  *  - Creates a 'Persist' object (which will perform the saving in the
241  *    background) and then starts it.
243  * @param aURL
244  *        The String representation of the URL of the document being saved
245  * @param aDocument
246  *        The document to be saved
247  * @param aDefaultFileName
248  *        The caller-provided suggested filename if we don't 
249  *        find a better one
250  * @param aContentDisposition
251  *        The caller-provided content-disposition header to use.
252  * @param aContentType
253  *        The caller-provided content-type to use
254  * @param aShouldBypassCache
255  *        If true, the document will always be refetched from the server
256  * @param aFilePickerTitleKey
257  *        Alternate title for the file picker
258  * @param aChosenData
259  *        If non-null this contains an instance of object AutoChosen (see below)
260  *        which holds pre-determined data so that the user does not need to be
261  *        prompted for a target filename.
262  * @param aReferrer
263  *        the referrer URI object (not URL string) to use, or null
264  *        if no referrer should be sent.
265  * @param aSkipPrompt [optional]
266  *        If set to true, we will attempt to save the file to the
267  *        default downloads folder without prompting.
268  * @param aCacheKey [optional]
269  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
270  *        allowed values.
271  */
272 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
273                       aContentType, aShouldBypassCache, aFilePickerTitleKey,
274                       aChosenData, aReferrer, aSkipPrompt, aCacheKey)
276   if (aSkipPrompt == undefined)
277     aSkipPrompt = false;
279   if (aCacheKey == undefined)
280     aCacheKey = null;
282   // Note: aDocument == null when this code is used by save-link-as...
283   var saveMode = GetSaveModeForContentType(aContentType);
284   var isDocument = aDocument != null && saveMode != SAVEMODE_FILEONLY;
285   var saveAsType = kSaveAsType_Complete;
287   var file, fileURL;
288   // Find the URI object for aURL and the FileName/Extension to use when saving.
289   // FileName/Extension will be ignored if aChosenData supplied.
290   var fileInfo = new FileInfo(aDefaultFileName);
291   if (aChosenData)
292     file = aChosenData.file;
293   else {
294     var charset = null;
295     if (aDocument)
296       charset = aDocument.characterSet;
297     else if (aReferrer)
298       charset = aReferrer.originCharset;
299     initFileInfo(fileInfo, aURL, charset, aDocument,
300                  aContentType, aContentDisposition);
301     var fpParams = {
302       fpTitleKey: aFilePickerTitleKey,
303       isDocument: isDocument,
304       fileInfo: fileInfo,
305       contentType: aContentType,
306       saveMode: saveMode,
307       saveAsType: saveAsType,
308       file: file,
309       fileURL: fileURL
310     };
312     if (!getTargetFile(fpParams, aSkipPrompt))
313       // If the method returned false this is because the user cancelled from
314       // the save file picker dialog.
315       return;
317     saveAsType = fpParams.saveAsType;
318     saveMode = fpParams.saveMode;
319     file = fpParams.file;
320     fileURL = fpParams.fileURL;
321   }
323   if (!fileURL)
324     fileURL = makeFileURI(file);
326   // XXX We depend on the following holding true in appendFiltersForContentType():
327   // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
328   // If we should save as text, the saveAsType is kSaveAsType_Text.
329   var useSaveDocument = isDocument &&
330                         (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) ||
331                          ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text)));
332   // If we're saving a document, and are saving either in complete mode or
333   // as converted text, pass the document to the web browser persist component.
334   // If we're just saving the HTML (second option in the list), send only the URI.
335   var source = useSaveDocument ? aDocument : fileInfo.uri;
336   var persistArgs = {
337     source      : source,
338     contentType : (!aChosenData && useSaveDocument &&
339                    saveAsType == kSaveAsType_Text) ?
340                   "text/plain" : null,
341     target      : fileURL,
342     postData    : isDocument ? getPostData() : null,
343     bypassCache : aShouldBypassCache
344   };
346   var persist = makeWebBrowserPersist();
348   // Calculate persist flags.
349   const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
350   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
351   if (aShouldBypassCache)
352     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
353   else
354     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
356   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
357   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
359   // Create download and initiate it (below)
360   var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer);
362   if (useSaveDocument) {
363     // Saving a Document, not a URI:
364     var filesFolder = null;
365     if (persistArgs.contentType != "text/plain") {
366       // Create the local directory into which to save associated files.
367       filesFolder = file.clone();
369       var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
370       var filesFolderLeafName = getStringBundle().formatStringFromName("filesFolder",
371                                                                        [nameWithoutExtension],
372                                                                        1);
374       filesFolder.leafName = filesFolderLeafName;
375     }
377     var encodingFlags = 0;
378     if (persistArgs.contentType == "text/plain") {
379       encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
380       encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
381       encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
382     }
383     else {
384       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
385     }
387     const kWrapColumn = 80;
388     tr.init((aChosenData ? aChosenData.uri : fileInfo.uri),
389             persistArgs.target, "", null, null, null, persist);
390     persist.progressListener = new DownloadListener(window, tr);
391     persist.saveDocument(persistArgs.source, persistArgs.target, filesFolder,
392                          persistArgs.contentType, encodingFlags, kWrapColumn);
393   } else {
394     tr.init((aChosenData ? aChosenData.uri : source),
395             persistArgs.target, "", null, null, null, persist);
396     persist.progressListener = new DownloadListener(window, tr);
397     persist.saveURI((aChosenData ? aChosenData.uri : source),
398                     aCacheKey, aReferrer, persistArgs.postData, null,
399                     persistArgs.target);
400   }
404  * Structure for holding info about automatically supplied parameters for
405  * internalSave(...). This allows parameters to be supplied so the user does not
406  * need to be prompted for file info.
407  * @param aFileAutoChosen This is an nsILocalFile object that has been
408  *        pre-determined as the filename for the target to save to
409  * @param aUriAutoChosen  This is the nsIURI object for the target
410  */
411 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
412   this.file = aFileAutoChosen;
413   this.uri  = aUriAutoChosen;
417  * Structure for holding info about a URL and the target filename it should be
418  * saved to. This structure is populated by initFileInfo(...).
419  * @param aSuggestedFileName This is used by initFileInfo(...) when it
420  *        cannot 'discover' the filename from the url 
421  * @param aFileName The target filename
422  * @param aFileBaseName The filename without the file extension
423  * @param aFileExt The extension of the filename
424  * @param aUri An nsIURI object for the url that is being saved
425  */
426 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) {
427   this.suggestedFileName = aSuggestedFileName;
428   this.fileName = aFileName;
429   this.fileBaseName = aFileBaseName;
430   this.fileExt = aFileExt;
431   this.uri = aUri;
435  * Determine what the 'default' filename string is, its file extension and the
436  * filename without the extension. This filename is used when prompting the user
437  * for confirmation in the file picker dialog.
438  * @param aFI A FileInfo structure into which we'll put the results of this method.
439  * @param aURL The String representation of the URL of the document being saved
440  * @param aURLCharset The charset of aURL.
441  * @param aDocument The document to be saved
442  * @param aContentType The content type we're saving, if it could be
443  *        determined by the caller.
444  * @param aContentDisposition The content-disposition header for the object
445  *        we're saving, if it could be determined by the caller.
446  */
447 function initFileInfo(aFI, aURL, aURLCharset, aDocument,
448                       aContentType, aContentDisposition)
450   try {
451     // Get an nsIURI object from aURL if possible:
452     try {
453       aFI.uri = makeURI(aURL, aURLCharset);
454       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
455       // populate extra object fields (eg filename and file extension).
456       var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL);
457       aFI.fileExt = url.fileExtension;
458     } catch (e) {
459     }
461     // Get the default filename:
462     aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName),
463                                       aFI.uri, aDocument, aContentDisposition);
464     // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
465     // if saveURL(...) was the original caller (hence both aContentType and
466     // aDocument are blank). If they were saving a link to a website then make
467     // the extension .htm .
468     if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) {
469       aFI.fileExt = "htm";
470       aFI.fileBaseName = aFI.fileName;
471     } else {
472       aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
473       aFI.fileBaseName = getFileBaseName(aFI.fileName);
474     }
475   } catch (e) {
476   }
479 Components.utils.import("resource://gre/modules/DownloadLastDir.jsm");
481 function getTargetFile(aFpP, /* optional */ aSkipPrompt)
483   const prefSvcContractID = "@mozilla.org/preferences-service;1";
484   const prefSvcIID = Components.interfaces.nsIPrefService;                              
485   var prefs = Components.classes[prefSvcContractID]
486                         .getService(prefSvcIID).getBranch("browser.download.");
488   const nsILocalFile = Components.interfaces.nsILocalFile;
490   var inPrivateBrowsing = false;
491   try {
492     var pbs = Components.classes["@mozilla.org/privatebrowsing;1"]
493                         .getService(Components.interfaces.nsIPrivateBrowsingService);
494     inPrivateBrowsing = pbs.privateBrowsingEnabled;
495   }
496   catch (e) {
497   }
499   // For information on download folder preferences, see
500   // mozilla/browser/components/preferences/main.js
501   
502   var useDownloadDir = prefs.getBoolPref("useDownloadDir");
503   var dir = null;
504   
505   // Default to lastDir if useDownloadDir is false, and lastDir
506   // is configured and valid. Otherwise, use the user's default
507   // downloads directory configured through download prefs.
508   var dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
509                           .getService(Components.interfaces.nsIDownloadManager);
510   try {                          
511     var lastDir = prefs.getComplexValue("lastDir", nsILocalFile);
512     if (inPrivateBrowsing && gDownloadLastDir.path)
513       lastDir = gDownloadLastDir.path;
514     if ((!aSkipPrompt || !useDownloadDir) && lastDir.exists())
515       dir = lastDir;
516     else
517       dir = dnldMgr.userDownloadsDirectory;
518   } catch(ex) {
519     dir = dnldMgr.userDownloadsDirectory;
520   }
522   if (!aSkipPrompt || !useDownloadDir || !dir || (dir && !dir.exists())) {
523     if (!dir || (dir && !dir.exists())) {
524       // Default to desktop.
525       var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
526                                   .getService(Components.interfaces.nsIProperties);
527       dir = fileLocator.get("Desk", nsILocalFile);
528     }
530     var fp = makeFilePicker();
531     var titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
532     var bundle = getStringBundle();
533     fp.init(window, bundle.GetStringFromName(titleKey), 
534             Components.interfaces.nsIFilePicker.modeSave);
535     
536     fp.defaultExtension = aFpP.fileInfo.fileExt;
537     fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName,
538                                              aFpP.fileInfo.fileExt);
539     appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt,
540                                 aFpP.saveMode);
542     if (dir)
543       fp.displayDirectory = dir;
544     
545     if (aFpP.isDocument) {
546       try {
547         fp.filterIndex = prefs.getIntPref("save_converter_index");
548       }
549       catch (e) {
550       }
551     }
553     if (fp.show() == Components.interfaces.nsIFilePicker.returnCancel || !fp.file)
554       return false;
556     // Do not store the last save directory as a pref inside the private browsing mode
557     var directory = fp.file.parent.QueryInterface(nsILocalFile);
558     if (inPrivateBrowsing)
559       gDownloadLastDir.path = directory;
560     else
561       prefs.setComplexValue("lastDir", nsILocalFile, directory);
563     fp.file.leafName = validateFileName(fp.file.leafName);
564     aFpP.saveAsType = fp.filterIndex;
565     aFpP.file = fp.file;
566     aFpP.fileURL = fp.fileURL;
568     if (aFpP.isDocument)
569       prefs.setIntPref("save_converter_index", aFpP.saveAsType);
570   }
571   else {
572     dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName,
573                                      aFpP.fileInfo.fileExt));
574     var file = dir;
575     
576     // Since we're automatically downloading, we don't get the file picker's 
577     // logic to check for existing files, so we need to do that here.
578     //
579     // Note - this code is identical to that in
580     //   mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
581     // If you are updating this code, update that code too! We can't share code
582     // here since that code is called in a js component.
583     var collisionCount = 0;
584     while (file.exists()) {
585       collisionCount++;
586       if (collisionCount == 1) {
587         // Append "(2)" before the last dot in (or at the end of) the filename
588         // special case .ext.gz etc files so we don't wind up with .tar(2).gz
589         if (file.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
590           file.leafName = file.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
591         else
592           file.leafName = file.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
593       }
594       else {
595         // replace the last (n) in the filename with (n+1)
596         file.leafName = file.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
597       }
598     }
599     aFpP.file = file;
600   }
602   return true;
605 // We have no DOM, and can only save the URL as is.
606 const SAVEMODE_FILEONLY      = 0x00;
607 // We have a DOM and can save as complete.
608 const SAVEMODE_COMPLETE_DOM  = 0x01;
609 // We have a DOM which we can serialize as text.
610 const SAVEMODE_COMPLETE_TEXT = 0x02;
612 // If we are able to save a complete DOM, the 'save as complete' filter
613 // must be the first filter appended.  The 'save page only' counterpart
614 // must be the second filter appended.  And the 'save as complete text'
615 // filter must be the third filter appended.
616 function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode)
618   var bundle = getStringBundle();
619   // The bundle name for saving only a specific content type.
620   var bundleName;
621   // The corresponding filter string for a specific content type.
622   var filterString;
624   // XXX all the cases that are handled explicitly here MUST be handled
625   // in GetSaveModeForContentType to return a non-fileonly filter.
626   switch (aContentType) {
627   case "text/html":
628     bundleName   = "WebPageHTMLOnlyFilter";
629     filterString = "*.htm; *.html";
630     break;
632   case "application/xhtml+xml":
633     bundleName   = "WebPageXHTMLOnlyFilter";
634     filterString = "*.xht; *.xhtml";
635     break;
637   case "image/svg+xml":
638     bundleName   = "WebPageSVGOnlyFilter";
639     filterString = "*.svg; *.svgz";
640     break;
642   case "text/xml":
643   case "application/xml":
644     bundleName   = "WebPageXMLOnlyFilter";
645     filterString = "*.xml";
646     break;
648   default:
649     if (aSaveMode != SAVEMODE_FILEONLY)
650       throw "Invalid save mode for type '" + aContentType + "'";
652     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
653     if (mimeInfo) {
655       var extEnumerator = mimeInfo.getFileExtensions();
657       var extString = "";
658       while (extEnumerator.hasMore()) {
659         var extension = extEnumerator.getNext();
660         if (extString)
661           extString += "; ";    // If adding more than one extension,
662                                 // separate by semi-colon
663         extString += "*." + extension;
664       }
666       if (extString)
667         aFilePicker.appendFilter(mimeInfo.description, extString);
668     }
670     break;
671   }
673   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
674     aFilePicker.appendFilter(bundle.GetStringFromName("WebPageCompleteFilter"), filterString);
675     // We should always offer a choice to save document only if
676     // we allow saving as complete.
677     aFilePicker.appendFilter(bundle.GetStringFromName(bundleName), filterString);
678   }
680   if (aSaveMode & SAVEMODE_COMPLETE_TEXT)
681     aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText);
683   // Always append the all files (*) filter
684   aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
687 function getPostData()
689   try {
690     var sessionHistory = getWebNavigation().sessionHistory;
691     return sessionHistory.getEntryAtIndex(sessionHistory.index, false)
692                          .QueryInterface(Components.interfaces.nsISHEntry)
693                          .postData;
694   }
695   catch (e) {
696   }
697   return null;
700 function getStringBundle()
702   return Components.classes["@mozilla.org/intl/stringbundle;1"]
703                    .getService(Components.interfaces.nsIStringBundleService)
704                    .createBundle("chrome://global/locale/contentAreaCommands.properties");
707 function makeWebBrowserPersist()
709   const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
710   const persistIID = Components.interfaces.nsIWebBrowserPersist;
711   return Components.classes[persistContractID].createInstance(persistIID);
715  * Constructs a new URI, using nsIIOService.
716  * @param aURL The URI spec.
717  * @param aOriginCharset The charset of the URI.
718  * @param aBaseURI Base URI to resolve aURL, or null.
719  * @return an nsIURI object based on aURL.
720  */
721 function makeURI(aURL, aOriginCharset, aBaseURI)
723   var ioService = Components.classes["@mozilla.org/network/io-service;1"]
724                             .getService(Components.interfaces.nsIIOService);
725   return ioService.newURI(aURL, aOriginCharset, aBaseURI);
728 function makeFileURI(aFile)
730   var ioService = Components.classes["@mozilla.org/network/io-service;1"]
731                             .getService(Components.interfaces.nsIIOService);
732   return ioService.newFileURI(aFile);
735 function makeFilePicker()
737   const fpContractID = "@mozilla.org/filepicker;1";
738   const fpIID = Components.interfaces.nsIFilePicker;
739   return Components.classes[fpContractID].createInstance(fpIID);
742 function getMIMEService()
744   const mimeSvcContractID = "@mozilla.org/mime;1";
745   const mimeSvcIID = Components.interfaces.nsIMIMEService;
746   const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID);
747   return mimeSvc;
750 // Given aFileName, find the fileName without the extension on the end.
751 function getFileBaseName(aFileName)
753   // Remove the file extension from aFileName:
754   return aFileName.replace(/\.[^.]*$/, "");
757 function getMIMETypeForURI(aURI)
759   try {
760     return getMIMEService().getTypeFromURI(aURI);
761   }
762   catch (e) {
763   }
764   return null;
767 function getMIMEInfoForType(aMIMEType, aExtension)
769   if (aMIMEType || aExtension) {
770     try {
771       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
772     }
773     catch (e) {
774     }
775   }
776   return null;
779 function getDefaultFileName(aDefaultFileName, aURI, aDocument,
780                             aContentDisposition)
782   // 1) look for a filename in the content-disposition header, if any
783   if (aContentDisposition) {
784     const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
785     const mhpIID = Components.interfaces.nsIMIMEHeaderParam;
786     const mhp = Components.classes[mhpContractID].getService(mhpIID);
787     var dummy = { value: null };  // Need an out param...
788     var charset = getCharsetforSave(aDocument);
790     var fileName = null;
791     try {
792       fileName = mhp.getParameter(aContentDisposition, "filename", charset,
793                                   true, dummy);
794     }
795     catch (e) {
796       try {
797         fileName = mhp.getParameter(aContentDisposition, "name", charset, true,
798                                     dummy);
799       }
800       catch (e) {
801       }
802     }
803     if (fileName)
804       return fileName;
805   }
807   try {
808     var url = aURI.QueryInterface(Components.interfaces.nsIURL);
809     if (url.fileName != "") {
810       // 2) Use the actual file name, if present
811       var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
812                                    .getService(Components.interfaces.nsITextToSubURI);
813       return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName));
814     }
815   } catch (e) {
816     // This is something like a data: and so forth URI... no filename here.
817   }
819   if (aDocument) {
820     var docTitle = validateFileName(aDocument.title).replace(/^\s+|\s+$/g, "");
821     if (docTitle) {
822       // 3) Use the document title
823       return docTitle;
824     }
825   }
827   if (aDefaultFileName)
828     // 4) Use the caller-provided name, if any
829     return validateFileName(aDefaultFileName);
831   // 5) If this is a directory, use the last directory name
832   var path = aURI.path.match(/\/([^\/]+)\/$/);
833   if (path && path.length > 1)
834     return validateFileName(path[1]);
836   try {
837     if (aURI.host)
838       // 6) Use the host.
839       return aURI.host;
840   } catch (e) {
841     // Some files have no information at all, like Javascript generated pages
842   }
843   try {
844     // 7) Use the default file name
845     return getStringBundle().GetStringFromName("DefaultSaveFileName");
846   } catch (e) {
847     //in case localized string cannot be found
848   }
849   // 8) If all else fails, use "index"
850   return "index";
853 function validateFileName(aFileName)
855   var re = /[\/]+/g;
856   if (navigator.appVersion.indexOf("Windows") != -1) {
857     re = /[\\\/\|]+/g;
858     aFileName = aFileName.replace(/[\"]+/g, "'");
859     aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
860     aFileName = aFileName.replace(/[\<]+/g, "(");
861     aFileName = aFileName.replace(/[\>]+/g, ")");
862   }
863   else if (navigator.appVersion.indexOf("Macintosh") != -1)
864     re = /[\:\/]+/g;
865   
866   return aFileName.replace(re, "_");
869 function getNormalizedLeafName(aFile, aDefaultExtension)
871   if (!aDefaultExtension)
872     return aFile;
874 #ifdef XP_WIN
875   // Remove trailing dots and spaces on windows
876   aFile = aFile.replace(/[\s.]+$/, "");
877 #endif
879   // Remove leading dots
880   aFile = aFile.replace(/^\.+/, "");
882   // Fix up the file name we're saving to to include the default extension
883   var i = aFile.lastIndexOf(".");
884   if (aFile.substr(i + 1) != aDefaultExtension)
885     return aFile + "." + aDefaultExtension;
887   return aFile;
890 function getDefaultExtension(aFilename, aURI, aContentType)
892   if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp")
893     return "";   // temporary fix for bug 120327
895   // First try the extension from the filename
896   const stdURLContractID = "@mozilla.org/network/standard-url;1";
897   const stdURLIID = Components.interfaces.nsIURL;
898   var url = Components.classes[stdURLContractID].createInstance(stdURLIID);
899   url.filePath = aFilename;
901   var ext = url.fileExtension;
903   // This mirrors some code in nsExternalHelperAppService::DoContent
904   // Use the filename first and then the URI if that fails
906   var mimeInfo = getMIMEInfoForType(aContentType, ext);
908   if (ext && mimeInfo && mimeInfo.extensionExists(ext))
909     return ext;
911   // Well, that failed.  Now try the extension from the URI
912   var urlext;
913   try {
914     url = aURI.QueryInterface(Components.interfaces.nsIURL);
915     urlext = url.fileExtension;
916   } catch (e) {
917   }
919   if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
920     return urlext;
921   }
922   else {
923     try {
924       if (mimeInfo)
925         return mimeInfo.primaryExtension;
926     }
927     catch (e) {
928     }
929     // Fall back on the extensions in the filename and URI for lack
930     // of anything better.
931     return ext || urlext;
932   }
935 function GetSaveModeForContentType(aContentType)
937   var saveMode = SAVEMODE_FILEONLY;
938   switch (aContentType) {
939   case "text/html":
940   case "application/xhtml+xml":
941   case "image/svg+xml":
942     saveMode |= SAVEMODE_COMPLETE_TEXT;
943     // Fall through
944   case "text/xml":
945   case "application/xml":
946     saveMode |= SAVEMODE_COMPLETE_DOM;
947     break;
948   }
950   return saveMode;
953 function getCharsetforSave(aDocument)
955   if (aDocument)
956     return aDocument.characterSet;
958   if (document.commandDispatcher.focusedWindow)
959     return document.commandDispatcher.focusedWindow.document.characterSet;
961   return window.content.document.characterSet;
965  * Open a URL from chrome, determining if we can handle it internally or need to
966  *  launch an external application to handle it.
967  * @param aURL The URL to be opened
968  */
969 function openURL(aURL)
971   var ios = Components.classes["@mozilla.org/network/io-service;1"]
972                       .getService(Components.interfaces.nsIIOService);
973   var uri = ios.newURI(aURL, null, null);
975   var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
976                               .getService(Components.interfaces.nsIExternalProtocolService);
978   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
979     // If we're not a browser, use the external protocol service to load the URI.
980     protocolSvc.loadUrl(uri);
981   }
982   else {
983     var loadgroup = Components.classes["@mozilla.org/network/load-group;1"]
984                               .createInstance(Components.interfaces.nsILoadGroup);
985     var appstartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
986                                .getService(Components.interfaces.nsIAppStartup);
988     var loadListener = {
989       onStartRequest: function ll_start(aRequest, aContext) {
990         appstartup.enterLastWindowClosingSurvivalArea();
991       },
992       onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) {
993         appstartup.exitLastWindowClosingSurvivalArea();
994       },
995       QueryInterface: function ll_QI(iid) {
996         if (iid.equals(Components.interfaces.nsISupports) ||
997             iid.equals(Components.interfaces.nsIRequestObserver) ||
998             iid.equals(Components.interfaces.nsISupportsWeakReference))
999           return this;
1000         throw Components.results.NS_ERROR_NO_INTERFACE;
1001       }
1002     }
1003     loadgroup.groupObserver = loadListener;
1005     var uriListener = {
1006       onStartURIOpen: function(uri) { return false; },
1007       doContent: function(ctype, preferred, request, handler) { return false; },
1008       isPreferred: function(ctype, desired) { return false; },
1009       canHandleContent: function(ctype, preferred, desired) { return false; },
1010       loadCookie: null,
1011       parentContentListener: null,
1012       getInterface: function(iid) {
1013         if (iid.equals(Components.interfaces.nsIURIContentListener))
1014           return this;
1015         if (iid.equals(Components.interfaces.nsILoadGroup))
1016           return loadgroup;
1017         throw Components.results.NS_ERROR_NO_INTERFACE;
1018       }
1019     }
1021     var channel = ios.newChannelFromURI(uri);
1022     var uriLoader = Components.classes["@mozilla.org/uriloader;1"]
1023                               .getService(Components.interfaces.nsIURILoader);
1024     uriLoader.openURI(channel, true, uriListener);
1025   }