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
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.
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 = {
46 delete this.ioService;
47 return this.ioService =
48 Components.classes["@mozilla.org/network/io-service;1"]
49 .getService(Components.interfaces.nsIIOService);
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");
62 * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal
63 * and checkLoadURIStrWithPrincipal.
64 * If |aPrincipal| is not allowed to link to |aURL|, this function throws with
68 * The URL a page has linked to. This could be passed either as a string
69 * or as a nsIURI object.
71 * The principal of the document from which aURL came.
73 * Flags to be passed to checkLoadURIStr. If undefined,
74 * nsIScriptSecurityManager.STANDARD will be passed.
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;
86 if (aURL instanceof Components.interfaces.nsIURI)
87 secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags);
89 secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags);
91 // XXXmano: dump the principal url here too
92 throw "Load of " + aURL + " denied.";
97 * Determine whether or not a given focused DOMWindow is in the content area.
99 function isContentFrame(aFocusedWindow)
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
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) {
147 var imageCache = Components.classes["@mozilla.org/image/cache;1"]
148 .getService(imgICache);
150 imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)));
152 contentType = props.get("type", nsISupportsCString);
153 contentDisposition = props.get("content-disposition",
157 // Failure to get type and content-disposition off the image is non-fatal
160 internalSave(aURL, null, aFileName, contentDisposition, contentType,
161 aShouldBypassCache, aFilePickerTitleKey, null, aReferrer,
165 function saveDocument(aDocument, aSkipPrompt)
168 throw "Must have a document when calling saveDocument";
170 // We want to use cached data because the document is currently visible.
172 aDocument.defaultView
173 .QueryInterface(Components.interfaces.nsIInterfaceRequestor);
175 var contentDisposition = null;
178 ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils)
179 .getDocumentMetadata("content-disposition");
181 // Failure to get a content-disposition is ok
187 ifreq.getInterface(Components.interfaces.nsIWebNavigation)
188 .QueryInterface(Components.interfaces.nsIWebPageDescriptor);
190 // We might not find it in the cache. Oh, well.
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) {
202 transfer[name].apply(transfer, arguments);
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);
215 DownloadListener.prototype = {
216 QueryInterface: function dl_qi(aIID)
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)) {
224 throw Components.results.NS_ERROR_NO_INTERFACE;
227 getInterface: function dl_gi(aIID)
229 if (aIID.equals(Components.interfaces.nsIAuthPrompt) ||
230 aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
232 Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
233 .getService(Components.interfaces.nsIPromptFactory);
234 return ww.getPrompt(this.window, aIID);
237 throw Components.results.NS_ERROR_NO_INTERFACE;
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,
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.
271 * The String representation of the URL of the document being saved
273 * The document to be saved
274 * @param aDefaultFileName
275 * The caller-provided suggested filename if we don't
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
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.
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
299 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
300 aContentType, aShouldBypassCache, aFilePickerTitleKey,
301 aChosenData, aReferrer, aSkipPrompt, aCacheKey)
303 if (aSkipPrompt == undefined)
306 if (aCacheKey == undefined)
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.
316 file = aChosenData.file;
317 sourceURI = aChosenData.uri;
318 saveAsType = kSaveAsType_Complete;
322 charset = aDocument.characterSet;
324 charset = aReferrer.originCharset;
325 var fileInfo = new FileInfo(aDefaultFileName);
326 initFileInfo(fileInfo, aURL, charset, aDocument,
327 aContentType, aContentDisposition);
328 sourceURI = fileInfo.uri;
331 fpTitleKey: aFilePickerTitleKey,
333 contentType: aContentType,
335 saveAsType: kSaveAsType_Complete,
339 if (!getTargetFile(fpParams, aSkipPrompt))
340 // If the method returned false this is because the user cancelled from
341 // the save file picker dialog.
344 saveAsType = fpParams.saveAsType;
345 file = fpParams.file;
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.
358 sourceURI : sourceURI,
359 sourceReferrer : aReferrer,
360 sourceDocument : useSaveDocument ? aDocument : null,
361 targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null,
363 sourceCacheKey : aCacheKey,
364 sourcePostData : aDocument ? getPostData(aDocument) : null,
365 bypassCache : aShouldBypassCache
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
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
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;
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;
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;
447 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
450 const kWrapColumn = 80;
451 persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder,
452 persistArgs.targetContentType, encodingFlags, kWrapColumn);
454 persist.saveURI(persistArgs.sourceURI,
455 persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null,
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
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
483 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) {
484 this.suggestedFileName = aSuggestedFileName;
485 this.fileName = aFileName;
486 this.fileBaseName = aFileBaseName;
487 this.fileExt = aFileExt;
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.
504 function initFileInfo(aFI, aURL, aURLCharset, aDocument,
505 aContentType, aContentDisposition)
508 // Get an nsIURI object from aURL if possible:
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;
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))) {
527 aFI.fileBaseName = aFI.fileName;
529 aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
530 aFI.fileBaseName = getFileBaseName(aFI.fileName);
537 * Given the Filepicker Parameters (aFpP), show the file picker dialog,
538 * prompting the user to confirm (or change) the fileName.
540 * A structure (see definition in internalSave(...) method)
541 * containing all the data used within this method.
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.
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;
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);
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()) {
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);
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,
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) {
615 fp.filterIndex = prefs.getIntPref("save_converter_index");
621 if (fp.show() == Components.interfaces.nsIFilePicker.returnCancel || !fp.file)
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);
633 aFpP.saveAsType = fp.filterIndex;
635 aFpP.fileURL = fp.fileURL;
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()) {
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)$&");
657 aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
660 // replace the last (n) in the filename with (n+1)
661 aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")");
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.
682 // The corresponding filter string for a specific content type.
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) {
689 bundleName = "WebPageHTMLOnlyFilter";
690 filterString = "*.htm; *.html";
693 case "application/xhtml+xml":
694 bundleName = "WebPageXHTMLOnlyFilter";
695 filterString = "*.xht; *.xhtml";
698 case "image/svg+xml":
699 bundleName = "WebPageSVGOnlyFilter";
700 filterString = "*.svg; *.svgz";
704 case "application/xml":
705 bundleName = "WebPageXMLOnlyFilter";
706 filterString = "*.xml";
710 if (aSaveMode != SAVEMODE_FILEONLY)
711 throw "Invalid save mode for type '" + aContentType + "'";
713 var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
716 var extEnumerator = mimeInfo.getFileExtensions();
719 while (extEnumerator.hasMore()) {
720 var extension = extEnumerator.getNext();
722 extString += "; "; // If adding more than one extension,
723 // separate by semi-colon
724 extString += "*." + extension;
728 aFilePicker.appendFilter(mimeInfo.description, extString);
734 if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
735 aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
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),
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)
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)
762 .QueryInterface(Components.interfaces.nsISHEntry);
763 return sessionHistoryEntry.postData;
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.
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);
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)
827 return getMIMEService().getTypeFromURI(aURI);
834 function getMIMEInfoForType(aMIMEType, aExtension)
836 if (aMIMEType || aExtension) {
838 return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
846 function getDefaultFileName(aDefaultFileName, aURI, aDocument,
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);
859 fileName = mhp.getParameter(aContentDisposition, "filename", charset,
864 fileName = mhp.getParameter(aContentDisposition, "name", charset, true,
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));
883 // This is something like a data: and so forth URI... no filename here.
887 var docTitle = validateFileName(aDocument.title).replace(/^\s+|\s+$/g, "");
889 // 3) Use the document title
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]);
908 // Some files have no information at all, like Javascript generated pages
911 // 7) Use the default file name
912 return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName");
914 //in case localized string cannot be found
916 // 8) If all else fails, use "index"
920 function validateFileName(aFileName)
923 if (navigator.appVersion.indexOf("Windows") != -1) {
925 aFileName = aFileName.replace(/[\"]+/g, "'");
926 aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
927 aFileName = aFileName.replace(/[\<]+/g, "(");
928 aFileName = aFileName.replace(/[\>]+/g, ")");
930 else if (navigator.appVersion.indexOf("Macintosh") != -1)
933 return aFileName.replace(re, "_");
936 function getNormalizedLeafName(aFile, aDefaultExtension)
938 if (!aDefaultExtension)
942 // Remove trailing dots and spaces on windows
943 aFile = aFile.replace(/[\s.]+$/, "");
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;
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))
978 // Well, that failed. Now try the extension from the URI
981 url = aURI.QueryInterface(Components.interfaces.nsIURL);
982 urlext = url.fileExtension;
986 if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
992 return mimeInfo.primaryExtension;
996 // Fall back on the extensions in the filename and URI for lack
997 // of anything better.
998 return ext || urlext;
1002 function GetSaveModeForContentType(aContentType, aDocument)
1004 // We can only save a complete page if we have a loaded document
1006 return SAVEMODE_FILEONLY;
1008 // Find the possible save modes using the provided content type
1009 var saveMode = SAVEMODE_FILEONLY;
1010 switch (aContentType) {
1012 case "application/xhtml+xml":
1013 case "image/svg+xml":
1014 saveMode |= SAVEMODE_COMPLETE_TEXT;
1017 case "application/xml":
1018 saveMode |= SAVEMODE_COMPLETE_DOM;
1025 function getCharsetforSave(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
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);
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();
1062 onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) {
1063 appstartup.exitLastWindowClosingSurvivalArea();
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))
1070 throw Components.results.NS_ERROR_NO_INTERFACE;
1073 loadgroup.groupObserver = loadListener;
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; },
1081 parentContentListener: null,
1082 getInterface: function(iid) {
1083 if (iid.equals(Components.interfaces.nsIURIContentListener))
1085 if (iid.equals(Components.interfaces.nsILoadGroup))
1087 throw Components.results.NS_ERROR_NO_INTERFACE;
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);