Bug 299372 - Content-Disposition headers no longer looked at for Save Link As filename. original-patch=dmose, r=mconnor, r=biesi, a=blocking-ff3+
authordolske@mozilla.com
Wed, 02 Apr 2008 20:02:08 -0700
changeset 13848 5541c4191c363404eb102f152dea2c2e9e97ff0e
parent 13847 274e6481a080c66d6ef05f49074e352d5aec0607
child 13849 0bff998ff2a872c132761003f2fb2a02e2447514
push idunknown
push userunknown
push dateunknown
reviewersmconnor, biesi, blocking-ff3
bugs299372
milestone1.9pre
Bug 299372 - Content-Disposition headers no longer looked at for Save Link As filename. original-patch=dmose, r=mconnor, r=biesi, a=blocking-ff3+
browser/app/profile/firefox.js
browser/base/content/nsContextMenu.js
browser/components/sessionstore/src/nsSessionStore.js
embedding/browser/activex/src/control/HelperAppDlg.cpp
embedding/browser/gtk/src/EmbedDownloadMgr.cpp
embedding/browser/photon/src/nsUnknownContentTypeHandler.cpp
embedding/components/ui/helperAppDlg/nsHelperAppDlg.js
toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
uriloader/base/nsURILoader.cpp
uriloader/base/nsURILoader.h
uriloader/exthandler/nsExternalHelperAppService.cpp
uriloader/exthandler/nsExternalHelperAppService.h
uriloader/exthandler/nsIExternalHelperAppService.idl
uriloader/exthandler/nsIHelperAppLauncherDialog.idl
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -219,16 +219,22 @@ pref("browser.urlbar.filter.javascript",
 // the maximum number of results to show in autocomplete when doing richResults
 pref("browser.urlbar.maxRichResults", 12);
 // Size of "chunks" affects the number of places to process between each search
 // timeout (ms). Too big and the UI will be unresponsive; too small and we'll
 // be waiting on the timeout too often without many results.
 pref("browser.urlbar.search.chunkSize", 1000);
 pref("browser.urlbar.search.timeout", 100);
 
+// Number of milliseconds to wait for the http headers (and thus
+// the Content-Disposition filename) before giving up and falling back to 
+// picking a filename without that info in hand so that the user sees some
+// feedback from their action.
+pref("browser.download.saveLinkAsFilenameTimeout", 1000);
+
 pref("browser.download.useDownloadDir", true);
 pref("browser.download.folderList", 0);
 pref("browser.download.manager.showAlertOnComplete", true);
 pref("browser.download.manager.showAlertInterval", 2000);
 pref("browser.download.manager.retention", 2);
 pref("browser.download.manager.showWhenStarting", true);
 pref("browser.download.manager.useWindow", true);
 pref("browser.download.manager.closeWhenDone", false);
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -36,16 +36,17 @@
 #   Tom Germeau <tom.germeau@epigoon.com>
 #   Jesse Ruderman <jruderman@gmail.com>
 #   Joe Hughes <joe@retrovirus.com>
 #   Pamela Greene <pamg.bugs@gmail.com>
 #   Michael Ventnor <ventnors_dogs234@yahoo.com.au>
 #   Simon B├╝nzli <zeniko@gmail.com>
 #   Gijs Kruitbosch <gijskruitbosch@gmail.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
+#   Dan Mosedale <dmose@mozilla.org>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -821,20 +822,133 @@ nsContextMenu.prototype = {
 
   // Save URL of clicked-on frame.
   saveFrame: function () {
     saveDocument(this.target.ownerDocument);
   },
 
   // Save URL of clicked-on link.
   saveLink: function() {
+    // canonical def in nsURILoader.h
+    const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
+    
     var doc =  this.target.ownerDocument;
     urlSecurityCheck(this.linkURL, doc.nodePrincipal);
-    saveURL(this.linkURL, this.linkText(), null, true, false,
-            doc.documentURIObject);
+    var linkText = this.linkText();
+    var linkURL = this.linkURL;
+
+
+    // an object to proxy the data through to
+    // nsIExternalHelperAppService.doContent, which will wait for the
+    // appropriate MIME-type headers and then prompt the user with a
+    // file picker
+    function saveAsListener() {}
+    saveAsListener.prototype = {
+      extListener: null, 
+
+      onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
+
+        // if the timer fired, the error status will have been caused by that,
+        // and we'll be restarting in onStopRequest, so no reason to notify
+        // the user
+        if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
+          return;
+
+        timer.cancel();
+
+        // some other error occured; notify the user...
+        if (!Components.isSuccessCode(aRequest.status)) {
+          try {
+            const sbs = Cc["@mozilla.org/intl/stringbundle;1"].
+                        getService(Ci.nsIStringBundleService);
+            const bundle = sbs.createBundle(
+                    "chrome://mozapps/locale/downloads/downloads.properties");
+            
+            const title = bundle.GetStringFromName("downloadErrorAlertTitle");
+            const msg = bundle.GetStringFromName("downloadErrorGeneric");
+            
+            const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+                              getService(Ci.nsIPromptService);
+            promptSvc.alert(doc.defaultView, title, msg);
+          } catch (ex) {}
+          return;
+        }
+
+        var extHelperAppSvc = 
+          Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
+          getService(Ci.nsIExternalHelperAppService);
+        var channel = aRequest.QueryInterface(Ci.nsIChannel);
+        this.extListener = 
+          extHelperAppSvc.doContent(channel.contentType, aRequest, 
+                                    doc.defaultView, true);
+        this.extListener.onStartRequest(aRequest, aContext);
+      }, 
+
+      onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext, 
+                                                       aStatusCode) {
+        if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
+          // do it the old fashioned way, which will pick the best filename
+          // it can without waiting.
+          saveURL(linkURL, linkText, null, true, false, doc.documentURIObject);
+        }
+        if (this.extListener)
+          this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+      },
+       
+      onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
+                                                           aInputStream,
+                                                           aOffset, aCount) {
+        this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
+                                         aOffset, aCount);
+      }
+    }
+
+    // in case we need to prompt the user for authentication
+    function callbacks() {}
+    callbacks.prototype = {
+      getInterface: function sLA_callbacks_getInterface(aIID) {
+        if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
+          var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+                   getService(Ci.nsIPromptFactory);
+          return ww.getPrompt(doc.defaultView, aIID);
+        }
+        throw Cr.NS_ERROR_NO_INTERFACE;
+      } 
+    }
+
+    // if it we don't have the headers after a short time, the user 
+    // won't have received any feedback from their click.  that's bad.  so
+    // we give up waiting for the filename. 
+    function timerCallback() {}
+    timerCallback.prototype = {
+      notify: function sLA_timer_notify(aTimer) {
+        channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+        return;
+      }
+    }
+
+    // set up a channel to do the saving
+    var ioService = Cc["@mozilla.org/network/io-service;1"].
+                    getService(Ci.nsIIOService);
+    var channel = ioService.newChannelFromURI(this.getLinkURI());
+    channel.notificationCallbacks = new callbacks();
+    channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE |
+                         Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
+    if (channel instanceof Ci.nsIHttpChannel)
+      channel.referrer = doc.documentURIObject;
+
+    // fallback to the old way if we don't see the headers quickly 
+    var timeToWait = 
+      gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout");
+    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    timer.initWithCallback(new timerCallback(), timeToWait,
+                           timer.TYPE_ONE_SHOT);
+
+    // kick off the channel with our proxy object as the listener
+    channel.asyncOpen(new saveAsListener(), null);
   },
 
   sendLink: function() {
     // we don't know the title of the link so pass in an empty string
     MailIntegration.sendMessage( this.linkURL, "" );
   },
 
   // Save URL of clicked-on image.
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -1888,17 +1888,19 @@ SessionStoreService.prototype = {
   saveState: function sss_saveState(aUpdateAll) {
     // if crash recovery is disabled, only save session resuming information
     if (!this._resume_from_crash && this._loadState == STATE_RUNNING)
       return;
     
     this._dirty = aUpdateAll;
     var oState = this._getCurrentState();
     oState.session = { state: ((this._loadState == STATE_RUNNING) ? STATE_RUNNING_STR : STATE_STOPPED_STR) };
-    this._writeFile(this._sessionFile, oState.toSource());
+    //var oStateString = this._toJSONString(oState);
+    var oStateString = oState.toSource();
+    this._writeFile(this._sessionFile, oStateString);
     this._lastSaveTime = Date.now();
   },
 
   /**
    * delete session datafile and backup
    */
   _clearDisk: function sss_clearDisk() {
     if (this._sessionFile.exists()) {
--- a/embedding/browser/activex/src/control/HelperAppDlg.cpp
+++ b/embedding/browser/activex/src/control/HelperAppDlg.cpp
@@ -469,16 +469,17 @@ CHelperAppLauncherDlg::Show(nsIHelperApp
 }
 
 /* nsILocalFile promptForSaveToFile (in nsIHelperAppLauncher aLauncher, in nsISupports aWindowContext, in wstring aDefaultFile, in wstring aSuggestedFileExtension); */
 NS_IMETHODIMP
 CHelperAppLauncherDlg::PromptForSaveToFile(nsIHelperAppLauncher *aLauncher, 
                                            nsISupports *aWindowContext, 
                                            const PRUnichar *aDefaultFile, 
                                            const PRUnichar *aSuggestedFileExtension, 
+                                           PRBool aForcePrompt, 
                                            nsILocalFile **_retval)
 {
     NS_ENSURE_ARG_POINTER(_retval);
     USES_CONVERSION;
 
     TCHAR szPath[MAX_PATH + 1];
     memset(szPath, 0, sizeof(szPath));
     _tcsncpy(szPath, W2T(aDefaultFile), MAX_PATH);
--- a/embedding/browser/gtk/src/EmbedDownloadMgr.cpp
+++ b/embedding/browser/gtk/src/EmbedDownloadMgr.cpp
@@ -207,16 +207,17 @@ EmbedDownloadMgr::GetDownloadInfo(nsIHel
 
   return NS_OK;
 }
 
 NS_IMETHODIMP EmbedDownloadMgr::PromptForSaveToFile(nsIHelperAppLauncher *aLauncher,
                                                     nsISupports *aWindowContext,
                                                     const PRUnichar *aDefaultFile,
                                                     const PRUnichar *aSuggestedFileExtension,
+                                                    PRBool aForcePrompt,
                                                     nsILocalFile **_retval)
 {
   *_retval = nsnull;
 
   nsCAutoString filePath;
   filePath.Assign(mDownload->file_name_with_path);
 
   nsCOMPtr<nsILocalFile> destFile;
--- a/embedding/browser/photon/src/nsUnknownContentTypeHandler.cpp
+++ b/embedding/browser/photon/src/nsUnknownContentTypeHandler.cpp
@@ -65,16 +65,17 @@ NS_IMETHODIMP nsUnknownContentTypeHandle
 {
 	return aLauncher->SaveToDisk( nsnull, PR_FALSE );
 }
 
 NS_IMETHODIMP nsUnknownContentTypeHandler::PromptForSaveToFile( nsIHelperAppLauncher* aLauncher,
                                                                 nsISupports *aWindowContext,
                                                                 const PRUnichar *aDefaultFile,
                                                                 const PRUnichar *aSuggestedFileExtension,
+                                                                PRBool aForcePrompt,
                                                                 nsILocalFile **_retval )
 {
 ///* ATENTIE */ printf("PromptForSaveToFile!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n");
 	NS_ENSURE_ARG_POINTER(_retval);
 	*_retval = nsnull;
 
 	/* try to get the PtMozillawidget_t* pointer form the aContext - use the fact the the WebBrowserContainer is
 		registering itself as nsIDocumentLoaderObserver ( SetDocLoaderObserver ) */
--- a/embedding/components/ui/helperAppDlg/nsHelperAppDlg.js
+++ b/embedding/components/ui/helperAppDlg/nsHelperAppDlg.js
@@ -114,17 +114,17 @@ nsHelperAppDialog.prototype = {
          this.mDialog.dialog = this;
          // Watch for error notifications.
          this.mIsMac = (this.mDialog.navigator.platform.indexOf( "Mac" ) != -1);
          this.progressListener.helperAppDlg = this;
          this.mLauncher.setWebProgressListener( this.progressListener );
     },
 
     // promptForSaveToFile:  Display file picker dialog and return selected file.
-    promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension) {
+    promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
         var result = "";
 
         const prefSvcContractID = "@mozilla.org/preferences-service;1";
         const prefSvcIID = Components.interfaces.nsIPrefService;
         var branch = Components.classes[prefSvcContractID].getService(prefSvcIID)
                                                           .getBranch("browser.download.");
         var dir = null;
 
@@ -137,17 +137,17 @@ nsHelperAppDialog.prototype = {
         } catch (e) { }
 
         var bundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                .getService(Components.interfaces.nsIStringBundleService)
                                .createBundle("chrome://global/locale/nsHelperAppDlg.properties");
 
         var autoDownload = branch.getBoolPref("autoDownload");
         // If the autoDownload pref is set then just download to default download directory
-        if (autoDownload && dir && dir.exists()) {
+        if (!aForcePrompt && autoDownload && dir && dir.exists()) {
             if (aDefaultFile == "")
                 aDefaultFile = bundle.GetStringFromName("noDefaultFile") + (aSuggestedFileExtension || "");
             dir.append(aDefaultFile);
             return uniqueFile(dir);
         }
 
         // Use file picker to show dialog.
         var nsIFilePicker = Components.interfaces.nsIFilePicker;
--- a/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
+++ b/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
@@ -136,43 +136,45 @@ nsUnknownContentTypeDialog.prototype = {
     //                       target filename (no target, therefore user must pick).
     //
     //                       Alternatively, if the user has selected to have all
     //                       files download to a specific location, return that
     //                       location and don't ask via the dialog. 
     //
     // Note - this function is called without a dialog, so it cannot access any part
     // of the dialog XUL as other functions on this object do. 
-    promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension) {
+    promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
       var result = null;
       
       this.mLauncher = aLauncher;
 
-      // Check to see if the user wishes to auto save to the default download
-      // folder without prompting.  This preferences may not be set, so default
-      // to not prompting.
       let prefs = Components.classes["@mozilla.org/preferences-service;1"]
                             .getService(Components.interfaces.nsIPrefBranch);
-      let autodownload = true;
-      try {
-        autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
-      } catch (e) { }
+
+      if (!aForcePrompt) {
+        // Check to see if the user wishes to auto save to the default download
+        // folder without prompting. Note that preference might not be set.
+        let autodownload = false;
+        try {
+          autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
+        } catch (e) { }
       
-      if (autodownload) {
-        // Retrieve the user's default download directory
-        var dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
-                                .getService(Components.interfaces.nsIDownloadManager);
-        var defaultFolder = dnldMgr.userDownloadsDirectory;
-        result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
+        if (autodownload) {
+          // Retrieve the user's default download directory
+          let dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
+                                  .getService(Components.interfaces.nsIDownloadManager);
+          let defaultFolder = dnldMgr.userDownloadsDirectory;
+          result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
+
+          // Check to make sure we have a valid directory, otherwise, prompt
+          if (result)
+            return result;
+        }
       }
       
-      // Check to make sure we have a valid directory, otherwise, prompt
-      if (result)
-        return result;
-      
       // Use file picker to show dialog.
       var nsIFilePicker = Components.interfaces.nsIFilePicker;
       var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
 
       var bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
       bundle = bundle.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
 
       var windowTitle = bundle.GetStringFromName("saveDialogTitle");
--- a/uriloader/base/nsURILoader.cpp
+++ b/uriloader/base/nsURILoader.cpp
@@ -589,16 +589,17 @@ nsresult nsDocumentOpenInfo::DispatchCon
     if (isGuessFromExt) {
       mContentType = APPLICATION_GUESS_FROM_EXT;
       aChannel->SetContentType(NS_LITERAL_CSTRING(APPLICATION_GUESS_FROM_EXT));
     }
 
     rv = helperAppService->DoContent(mContentType,
                                      request,
                                      m_originalContext,
+                                     PR_FALSE,
                                      getter_AddRefs(m_targetStreamListener));
     if (NS_FAILED(rv)) {
       request->SetLoadFlags(loadFlags);
       m_targetStreamListener = nsnull;
     }
   }
       
   NS_ASSERTION(m_targetStreamListener || NS_FAILED(rv),
--- a/uriloader/base/nsURILoader.h
+++ b/uriloader/base/nsURILoader.h
@@ -94,9 +94,15 @@ protected:
 
 /**
  * The load has been cancelled because it was found on a malware or phishing blacklist.
  * XXX: this belongs in an nsDocShellErrors.h file of some sort.
  */
 #define NS_ERROR_MALWARE_URI   NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_URILOADER, 30)
 #define NS_ERROR_PHISHING_URI  NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_URILOADER, 31)
 
+/**
+ * Used when "Save Link As..." doesn't see the headers quickly enough to choose
+ * a filename.  See nsContextMenu.js. 
+ */
+#define NS_ERROR_SAVE_LINK_AS_TIMEOUT  NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_URILOADER, 32);
+
 #endif /* nsURILoader_h__ */
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -519,16 +519,17 @@ nsresult nsExternalHelperAppService::Ini
 nsExternalHelperAppService::~nsExternalHelperAppService()
 {
   gExtProtSvc = nsnull;
 }
 
 NS_IMETHODIMP nsExternalHelperAppService::DoContent(const nsACString& aMimeContentType,
                                                     nsIRequest *aRequest,
                                                     nsIInterfaceRequestor *aWindowContext,
+                                                    PRBool aForceSave,
                                                     nsIStreamListener ** aStreamListener)
 {
   nsAutoString fileName;
   nsCAutoString fileExtension;
   PRUint32 reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
   nsresult rv;
 
   // Get the file extension and name that we will need later
@@ -635,17 +636,18 @@ NS_IMETHODIMP nsExternalHelperAppService
   // nsExternalAppHandler
   nsCAutoString buf;
   mimeInfo->GetPrimaryExtension(buf);
 
   nsExternalAppHandler * handler = new nsExternalAppHandler(mimeInfo,
                                                             buf,
                                                             aWindowContext,
                                                             fileName,
-                                                            reason);
+                                                            reason,
+                                                            aForceSave);
   if (!handler)
     return NS_ERROR_OUT_OF_MEMORY;
   NS_ADDREF(*aStreamListener = handler);
   
   return NS_OK;
 }
 
 NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(const nsACString& aExtension,
@@ -986,21 +988,22 @@ NS_INTERFACE_MAP_BEGIN(nsExternalAppHand
    NS_INTERFACE_MAP_ENTRY(nsICancelable)
    NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
 NS_INTERFACE_MAP_END_THREADSAFE
 
 nsExternalAppHandler::nsExternalAppHandler(nsIMIMEInfo * aMIMEInfo,
                                            const nsCSubstring& aTempFileExtension,
                                            nsIInterfaceRequestor* aWindowContext,
                                            const nsAString& aSuggestedFilename,
-                                           PRUint32 aReason)
+                                           PRUint32 aReason, PRBool aForceSave)
 : mMimeInfo(aMIMEInfo)
 , mWindowContext(aWindowContext)
 , mWindowToClose(nsnull)
 , mSuggestedFileName(aSuggestedFilename)
+, mForceSave(aForceSave)
 , mCanceled(PR_FALSE)
 , mShouldCloseWindow(PR_FALSE)
 , mReceivedDispositionInfo(PR_FALSE)
 , mStopRequestIssued(PR_FALSE)
 , mProgressListenerInitialized(PR_FALSE)
 , mReason(aReason)
 , mContentLength(-1)
 , mProgress(0)
@@ -1465,16 +1468,23 @@ NS_IMETHODIMP nsExternalAppHandler::OnSt
 
   // OK, now check why we're here
   if (!alwaysAsk && mReason != nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
     // Force asking if we're not saving.  See comment back when we fetched the
     // alwaysAsk boolean for details.
     alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
   }
 
+  // if we were told that we _must_ save to disk without asking, all the stuff
+  // before this is irrelevant; override it
+  if (mForceSave) {
+    alwaysAsk = PR_FALSE;
+    action = nsIMIMEInfo::saveToDisk;
+  }
+  
   if (alwaysAsk)
   {
     // do this first! make sure we don't try to take an action until the user tells us what they want to do
     // with it...
     mReceivedDispositionInfo = PR_FALSE; 
 
     // invoke the dialog!!!!! use mWindowContext as the window context parameter for the dialog request
     mDialog = do_CreateInstance( NS_IHELPERAPPLAUNCHERDLG_CONTRACTID, &rv );
@@ -1899,17 +1909,17 @@ nsresult nsExternalAppHandler::PromptFor
   // released, which would release this object too, which would crash.
   // See Bug 249143
   nsRefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
   nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
   rv = mDialog->PromptForSaveToFile(this, 
                                     mWindowContext,
                                     aDefaultFile.get(),
                                     aFileExtension.get(),
-                                    aNewFile);
+                                    mForceSave, aNewFile);
   return rv;
 }
 
 nsresult nsExternalAppHandler::MoveFile(nsIFile * aNewFileLocation)
 {
   nsresult rv = NS_OK;
   NS_ASSERTION(mStopRequestIssued, "uhoh, how did we get here if we aren't done getting data?");
  
--- a/uriloader/exthandler/nsExternalHelperAppService.h
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -246,17 +246,17 @@ public:
    * @param aWindowContext Window context, as passed to DoContent
    * @param aFileName      The filename to use
    * @param aReason        A constant from nsIHelperAppLauncherDialog indicating
    *                       why the request is handled by a helper app.
    */
   nsExternalAppHandler(nsIMIMEInfo * aMIMEInfo, const nsCSubstring& aFileExtension,
                        nsIInterfaceRequestor * aWindowContext,
                        const nsAString& aFilename,
-                       PRUint32 aReason);
+                       PRUint32 aReason, PRBool aForceSave);
 
   ~nsExternalAppHandler();
 
 protected:
   nsCOMPtr<nsIFile> mTempFile;
   nsCOMPtr<nsIURI> mSourceUrl;
   nsString mTempFileExtension;
   /**
@@ -276,16 +276,23 @@ protected:
   /**
    * The following field is set if we were processing an http channel that had
    * a content disposition header which specified the SUGGESTED file name we
    * should present to the user in the save to disk dialog. 
    */
   nsString mSuggestedFileName;
 
   /**
+   * If set, this handler should forcibly save the file to disk regardless of
+   * MIME info settings or anything else, without ever popping up the 
+   * unknown content type handling dialog.
+   */
+  PRPackedBool mForceSave;
+  
+  /**
    * The canceled flag is set if the user canceled the launching of this
    * application before we finished saving the data to a temp file.
    */
   PRPackedBool mCanceled;
 
   /**
    * This is set based on whether the channel indicates that a new window
    * was opened specifically for this download.  If so, then we
--- a/uriloader/exthandler/nsIExternalHelperAppService.idl
+++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
@@ -46,33 +46,36 @@ interface nsIFile;
 interface nsIMIMEInfo;
 interface nsIWebProgressListener2;
 interface nsIInterfaceRequestor;
 
 /**
  * The external helper app service is used for finding and launching
  * platform specific external applications for a given mime content type.
  */
-[scriptable, uuid(0ea90cf3-2dd9-470f-8f76-f141743c5678)]
+[scriptable, uuid(9e456297-ba3e-42b1-92bd-b7db014268cb)]
 interface nsIExternalHelperAppService : nsISupports
 {
   /**
    * Binds an external helper application to a stream listener. The caller
    * should pump data into the returned stream listener. When the OnStopRequest
    * is issued, the stream listener implementation will launch the helper app
    * with this data.
    * @param aMimeContentType The content type of the incoming data
    * @param aRequest The request corresponding to the incoming data
    * @param aWindowContext Use GetInterface to retrieve properties like the
    *                       dom window or parent window...
    *                       The service might need this in order to bring up dialogs.
+   * @param aForceSave True to always save this content to disk, regardless of
+   *                   nsIMIMEInfo and other such influences.
    * @return A nsIStreamListener which the caller should pump the data into.
    */
   nsIStreamListener doContent (in ACString aMimeContentType, in nsIRequest aRequest,
-                               in nsIInterfaceRequestor aWindowContext); 
+                               in nsIInterfaceRequestor aWindowContext,
+                               in boolean aForceSave); 
 
   /**
    * Returns true if data from a URL with this extension combination
    * is to be decoded from aEncodingType prior to saving or passing
    * off to helper apps, false otherwise.
    */
   boolean applyDecodingForExtension(in AUTF8String aExtension,
                                     in ACString aEncodingType);
--- a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl
+++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl
@@ -48,17 +48,17 @@ interface nsILocalFile;
  * Usage:  Clients (of which there is one: the nsIExternalHelperAppService
  * implementation in mozilla/uriloader/exthandler) create an instance of
  * this interface (using the contract ID) and then call the show() method.
  *
  * The dialog is shown non-modally.  The implementation of the dialog
  * will access methods of the nsIHelperAppLauncher passed in to show()
  * in order to cause a "save to disk" or "open using" action.
  */
-[scriptable, uuid(64355793-988d-40a5-ba8e-fcde78cac631)]
+[scriptable, uuid(f3704fdc-8ae6-4eba-a3c3-f02958ac0649)]
 interface nsIHelperAppLauncherDialog : nsISupports {
   /**
    * This request is passed to the helper app dialog because Gecko can not
    * handle content of this type.
    */
   const unsigned long REASON_CANTHANDLE = 0;
 
   /**
@@ -67,32 +67,54 @@ interface nsIHelperAppLauncherDialog : n
   const unsigned long REASON_SERVERREQUEST = 1;
 
   /**
    * Gecko detected that the type sent by the server (e.g. text/plain) does
    * not match the actual type.
    */
   const unsigned long REASON_TYPESNIFFED = 2;
 
-  // Show confirmation dialog for launching application (or "save to
-  // disk") for content specified by aLauncher.
-  // aReason is one of the constants from above. It indicates why the dialog is
-  // shown.
-  // Implementors should treat unknown reasons like REASON_CANTHANDLE.
+  /**
+   * Show confirmation dialog for launching application (or "save to
+   * disk") for content specified by aLauncher.
+   *
+   * @param aLauncher
+   *        A nsIHelperAppLauncher to be invoked when a file is selected.
+   * @param aWindowContext
+   *        Window associated with action.
+   * @param aReason
+   *        One of the constants from above. It indicates why the dialog is
+   *        shown. Implementors should treat unknown reasons like
+   *        REASON_CANTHANDLE.
+   */
   void show(in nsIHelperAppLauncher aLauncher,
-            in nsISupports aContext,
+            in nsISupports aWindowContext,
             in unsigned long aReason);
-	
-	// invoke a save to file dialog instead of the full fledged helper app dialog.
-	// aDefaultFileName --> default file name to provide (can be null)
-	// aSuggestedFileExtension --> sugested file extension
-	// aFileLocation --> return value for the file location
+
+  /**
+   * Invoke a save-to-file dialog instead of the full fledged helper app dialog.
+   * Returns the a nsILocalFile for the file name/location selected.
+   *
+   * @param aLauncher
+   *        A nsIHelperAppLauncher to be invoked when a file is selected.
+   * @param aWindowContext
+   *        Window associated with action.
+   * @param aDefaultFileName
+   *        Default file name to provide (can be null)
+   * @param aSuggestedFileExtension
+   *        Sugested file extension
+   * @param aForcePrompt
+   *        Set to true to force prompting the user for thet file
+   *        name/location, otherwise perferences may control if the user is
+   *        prompted.
+   */
   nsILocalFile promptForSaveToFile(in nsIHelperAppLauncher aLauncher, 
                                    in nsISupports aWindowContext, 
-                                   in wstring aDefaultFile, 
-                                   in wstring aSuggestedFileExtension);
+                                   in wstring aDefaultFileName, 
+                                   in wstring aSuggestedFileExtension,
+                                   in boolean aForcePrompt);
 };
 
 
 %{C++
 #define NS_IHELPERAPPLAUNCHERDLG_CONTRACTID    "@mozilla.org/helperapplauncherdialog;1"
 #define NS_IHELPERAPPLAUNCHERDLG_CLASSNAME "Mozilla Helper App Launcher Confirmation Dialog"
 %}