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