Backed out 14 changesets (bug 1746052) for causing build bustages in nsExternalHelperAppService.cpp CLOSED TREE
authorNoemi Erli <nerli@mozilla.com>
Thu, 05 May 2022 23:13:33 +0300
changeset 616342 53107941679965745bfe12311b44f411aaccb5a7
parent 616341 bf46b0add531503bb1f37b51c3cb80552d6adba2
child 616343 e6d2fef65b87d2eeeac54c1c86f28a8236f709e3
push id39657
push userimoraru@mozilla.com
push dateFri, 06 May 2022 09:50:52 +0000
treeherdermozilla-central@fe0b18ac5fe1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1746052
milestone102.0a1
backs outbf46b0add531503bb1f37b51c3cb80552d6adba2
684b2aca10bbbb04b928b355da256db51a6dd8e7
d4796eeeaf64bc2551dc32d7fc3d0eb1094b76b0
5cf2378f6eb4f1e6263b1a8510b0adbed5e6b61f
2055ec1e9a57950f4798b97ea98c314b51053022
1398fc0669d2a847f35a996e9321c001926a0ee5
1f979899c843cb6092e72ea1948499e433f1ded1
561a24801d4d8bbfadc79d8c225007ff9936d3ef
73e4689120f0aad1ebd6961ed8fea19c95faeada
9891f3b0229b8fe79c2ff0a8c9b4af2b932b8e8f
4c1ab30de0bdb5255590493f5335ea771a526240
108e14122e53aa9e9653b582ca22ddaf979e075e
1ee59c52578a4892c93e646ba551d68da47cfd0e
41fe77c5f4b2d059b05b50a3ea68c839485d849a
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 14 changesets (bug 1746052) for causing build bustages in nsExternalHelperAppService.cpp CLOSED TREE Backed out changeset bf46b0add531 (bug 1746052) Backed out changeset 684b2aca10bb (bug 1746052) Backed out changeset d4796eeeaf64 (bug 1746052) Backed out changeset 5cf2378f6eb4 (bug 1746052) Backed out changeset 2055ec1e9a57 (bug 1746052) Backed out changeset 1398fc0669d2 (bug 1746052) Backed out changeset 1f979899c843 (bug 1746052) Backed out changeset 561a24801d4d (bug 1746052) Backed out changeset 73e4689120f0 (bug 1746052) Backed out changeset 9891f3b0229b (bug 1746052) Backed out changeset 4c1ab30de0bd (bug 1746052) Backed out changeset 108e14122e53 (bug 1746052) Backed out changeset 1ee59c52578a (bug 1746052) Backed out changeset 41fe77c5f4b2 (bug 1746052)
browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js
dom/base/nsContentAreaDragDrop.cpp
dom/base/nsCopySupport.cpp
dom/webbrowserpersist/nsWebBrowserPersist.cpp
image/imgIRequest.idl
image/imgRequest.cpp
image/imgRequest.h
image/imgRequestProxy.cpp
netwerk/mime/nsIMIMEService.idl
toolkit/components/downloads/DownloadPaths.jsm
toolkit/components/downloads/test/unit/test_DownloadPaths.js
toolkit/content/contentAreaUtils.js
toolkit/mozapps/downloads/HelperAppDlg.jsm
uriloader/exthandler/nsExternalHelperAppService.cpp
uriloader/exthandler/nsExternalHelperAppService.h
uriloader/exthandler/tests/mochitest/browser.ini
uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
uriloader/exthandler/tests/mochitest/browser_save_filenames.js
uriloader/exthandler/tests/mochitest/save_filenames.html
uriloader/exthandler/tests/unit/test_filename_sanitize.js
uriloader/exthandler/tests/unit/xpcshell.ini
widget/windows/nsDataObj.cpp
xpcom/base/nsCRTGlue.h
xpcom/io/nsLocalFileWin.cpp
xpcom/io/nsLocalFileWin.h
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js
@@ -95,17 +95,17 @@ function test() {
         "gDownloadLastDir should be the expected global last dir"
       );
 
       launcher.saveDestinationAvailable = null;
       aWin.close();
       aCallback();
     };
 
-    launcherDialog.promptForSaveToFileAsync(launcher, aWin, "", "", false);
+    launcherDialog.promptForSaveToFileAsync(launcher, aWin, null, null, null);
   }
 
   testOnWindow(false, function(win, downloadDir) {
     testDownloadDir(win, downloadDir, file1, tmpDir, dir1, dir1, function() {
       testOnWindow(true, function(win1, downloadDir1) {
         testDownloadDir(
           win1,
           downloadDir1,
--- a/dom/base/nsContentAreaDragDrop.cpp
+++ b/dom/base/nsContentAreaDragDrop.cpp
@@ -256,33 +256,52 @@ nsContentAreaDragDropDataProvider::GetFl
     // content processes.
     if (XRE_IsParentProcess()) {
       rv = aTransferable->GetTransferData(kImageRequestMime,
                                           getter_AddRefs(tmp));
       NS_ENSURE_SUCCESS(rv, rv);
       supportsString = do_QueryInterface(tmp);
       if (!supportsString) return NS_ERROR_FAILURE;
 
-      nsAutoString contentType;
-      supportsString->GetData(contentType);
+      nsAutoString imageRequestMime;
+      supportsString->GetData(imageRequestMime);
 
-      nsCOMPtr<nsIMIMEService> mimeService =
-          do_GetService("@mozilla.org/mime;1");
-      if (NS_WARN_IF(!mimeService)) {
-        return NS_ERROR_FAILURE;
-      }
+      // If we have a MIME type, check the extension is compatible
+      if (!imageRequestMime.IsEmpty()) {
+        // Build a URL to get the filename extension
+        nsCOMPtr<nsIURL> imageURL = do_QueryInterface(sourceURI, &rv);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        nsAutoCString extension;
+        rv = imageURL->GetFileExtension(extension);
+        NS_ENSURE_SUCCESS(rv, rv);
 
-      mimeService->ValidateFileNameForSaving(
-          targetFilename, NS_ConvertUTF16toUTF8(contentType),
-          nsIMIMEService::VALIDATE_DEFAULT, targetFilename);
-    } else {
-      // make the filename safe for the filesystem
-      targetFilename.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS,
-                                 '-');
+        NS_ConvertUTF16toUTF8 mimeCString(imageRequestMime);
+        bool isValidExtension;
+        nsAutoCString primaryExtension;
+        rv = CheckAndGetExtensionForMime(extension, mimeCString,
+                                         &isValidExtension, &primaryExtension);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        if (!isValidExtension && !primaryExtension.IsEmpty()) {
+          // The filename extension is missing or incompatible
+          // with the MIME type, replace it with the primary
+          // extension.
+          nsAutoCString newFileName;
+          rv = imageURL->GetFileBaseName(newFileName);
+          NS_ENSURE_SUCCESS(rv, rv);
+          newFileName.Append(".");
+          newFileName.Append(primaryExtension);
+          CopyUTF8toUTF16(newFileName, targetFilename);
+        }
+      }
     }
+    // make the filename safe for the filesystem
+    targetFilename.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS,
+                               '-');
 #endif /* defined(XP_MACOSX) */
 
     // get the target directory from the kFilePromiseDirectoryMime
     // flavor
     nsCOMPtr<nsISupports> dirPrimitive;
     rv = aTransferable->GetTransferData(kFilePromiseDirectoryMime,
                                         getter_AddRefs(dirPrimitive));
     NS_ENSURE_SUCCESS(rv, rv);
@@ -412,37 +431,64 @@ nsresult DragDataProducer::GetImageData(
     NS_ENSURE_SUCCESS(rv, rv);
 
     // pass out the image source string
     CopyUTF8toUTF16(spec, mImageSourceString);
 
     nsCString mimeType;
     aRequest->GetMimeType(getter_Copies(mimeType));
 
-    nsAutoCString fileName;
-    aRequest->GetFileName(fileName);
-
 #if defined(XP_MACOSX)
     // Save the MIME type so we can make sure the extension
     // is compatible (and replace it if it isn't) when the
     // image is dropped. On Mac, we need to get the OS MIME
     // handler information in the parent due to sandboxing.
     CopyUTF8toUTF16(mimeType, mImageRequestMime);
-    CopyUTF8toUTF16(fileName, mImageDestFileName);
 #else
     nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
     if (NS_WARN_IF(!mimeService)) {
       return NS_ERROR_FAILURE;
     }
 
+    nsCOMPtr<nsIMIMEInfo> mimeInfo;
+    mimeService->GetFromTypeAndExtension(mimeType, ""_ns,
+                                         getter_AddRefs(mimeInfo));
+    if (mimeInfo) {
+      nsAutoCString extension;
+      imgUrl->GetFileExtension(extension);
+
+      bool validExtension;
+      if (extension.IsEmpty() ||
+          NS_FAILED(mimeInfo->ExtensionExists(extension, &validExtension)) ||
+          !validExtension) {
+        // Fix the file extension in the URL
+        nsAutoCString primaryExtension;
+        mimeInfo->GetPrimaryExtension(primaryExtension);
+        if (!primaryExtension.IsEmpty()) {
+          rv = NS_MutateURI(imgUrl)
+                   .Apply(&nsIURLMutator::SetFileExtension, primaryExtension,
+                          nullptr)
+                   .Finalize(imgUrl);
+          NS_ENSURE_SUCCESS(rv, rv);
+        }
+      }
+    }
+#endif /* defined(XP_MACOSX) */
+
+    nsAutoCString fileName;
+    imgUrl->GetFileName(fileName);
+
+    NS_UnescapeURL(fileName);
+
+#if !defined(XP_MACOSX)
+    // make the filename safe for the filesystem
+    fileName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '-');
+#endif
+
     CopyUTF8toUTF16(fileName, mImageDestFileName);
-    mimeService->ValidateFileNameForSaving(mImageDestFileName, mimeType,
-                                           nsIMIMEService::VALIDATE_DEFAULT,
-                                           mImageDestFileName);
-#endif
 
     // and the image object
     mImage = aImage;
   }
 
   return NS_OK;
 }
 
--- a/dom/base/nsCopySupport.cpp
+++ b/dom/base/nsCopySupport.cpp
@@ -27,16 +27,18 @@
 #include "nsIContentViewerEdit.h"
 #include "nsISelectionController.h"
 
 #include "nsPIDOMWindow.h"
 #include "mozilla/dom/Document.h"
 #include "nsHTMLDocument.h"
 #include "nsGkAtoms.h"
 #include "nsIFrame.h"
+#include "nsIURI.h"
+#include "nsGenericHTMLElement.h"
 
 // image copy stuff
 #include "nsIImageLoadingContent.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsContentUtils.h"
 #include "nsContentCID.h"
 
 #ifdef XP_WIN
@@ -597,49 +599,80 @@ static nsresult AppendImagePromise(nsITr
   NS_ENSURE_SUCCESS(rv, rv);
   if (isMultipart) {
     return NS_OK;
   }
 
   nsCOMPtr<nsINode> node = do_QueryInterface(aImageElement, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
-  if (NS_WARN_IF(!mimeService)) {
-    return NS_ERROR_FAILURE;
-  }
+  // Fix the file extension in the URL if necessary
+  nsCOMPtr<nsIMIMEService> mimeService =
+      do_GetService(NS_MIMESERVICE_CONTRACTID);
+  NS_ENSURE_TRUE(mimeService, NS_OK);
 
   nsCOMPtr<nsIURI> imgUri;
   rv = aImgRequest->GetFinalURI(getter_AddRefs(imgUri));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  nsCOMPtr<nsIURL> imgUrl = do_QueryInterface(imgUri);
+  NS_ENSURE_TRUE(imgUrl, NS_OK);
+
+  nsAutoCString extension;
+  rv = imgUrl->GetFileExtension(extension);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCString mimeType;
+  rv = aImgRequest->GetMimeType(getter_Copies(mimeType));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIMIMEInfo> mimeInfo;
+  mimeService->GetFromTypeAndExtension(mimeType, ""_ns,
+                                       getter_AddRefs(mimeInfo));
+  NS_ENSURE_TRUE(mimeInfo, NS_OK);
+
   nsAutoCString spec;
-  rv = imgUri->GetSpec(spec);
+  rv = imgUrl->GetSpec(spec);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // pass out the image source string
   nsString imageSourceString;
   CopyUTF8toUTF16(spec, imageSourceString);
 
-  nsCString mimeType;
-  rv = aImgRequest->GetMimeType(getter_Copies(mimeType));
-  NS_ENSURE_SUCCESS(rv, rv);
+  bool validExtension;
+  if (extension.IsEmpty() ||
+      NS_FAILED(mimeInfo->ExtensionExists(extension, &validExtension)) ||
+      !validExtension) {
+    // Fix the file extension in the URL
+    nsAutoCString primaryExtension;
+    mimeInfo->GetPrimaryExtension(primaryExtension);
+    if (!primaryExtension.IsEmpty()) {
+      rv = NS_MutateURI(imgUri)
+               .Apply(&nsIURLMutator::SetFileExtension, primaryExtension,
+                      nullptr)
+               .Finalize(imgUrl);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
 
   nsAutoCString fileName;
-  rv = aImgRequest->GetFileName(fileName);
-  NS_ENSURE_SUCCESS(rv, rv);
+  imgUrl->GetFileName(fileName);
+
+  NS_UnescapeURL(fileName);
 
-  nsAutoString validFileName = NS_ConvertUTF8toUTF16(fileName);
-  mimeService->ValidateFileNameForSaving(
-      validFileName, mimeType, nsIMIMEService::VALIDATE_DEFAULT, validFileName);
+  // make the filename safe for the filesystem
+  fileName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '-');
+
+  nsString imageDestFileName;
+  CopyUTF8toUTF16(fileName, imageDestFileName);
 
   rv = AppendString(aTransferable, imageSourceString, kFilePromiseURLMime);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = AppendString(aTransferable, validFileName, kFilePromiseDestFilename);
+  rv = AppendString(aTransferable, imageDestFileName, kFilePromiseDestFilename);
   NS_ENSURE_SUCCESS(rv, rv);
 
   aTransferable->SetRequestingPrincipal(node->NodePrincipal());
   aTransferable->SetContentPolicyType(nsIContentPolicy::TYPE_INTERNAL_IMAGE);
 
   // add the dataless file promise flavor
   return aTransferable->AddDataFlavor(kFilePromiseMime);
 }
--- a/dom/webbrowserpersist/nsWebBrowserPersist.cpp
+++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp
@@ -2147,36 +2147,88 @@ nsresult nsWebBrowserPersist::CalculateA
 
   // Get the content type from the MIME service
   if (contentType.IsEmpty()) {
     nsCOMPtr<nsIURI> uri;
     aChannel->GetOriginalURI(getter_AddRefs(uri));
     mMIMEService->GetTypeFromURI(uri, contentType);
   }
 
-  // Validate the filename
+  // Append the extension onto the file
   if (!contentType.IsEmpty()) {
-    nsAutoString newFileName;
-    if (NS_SUCCEEDED(mMIMEService->GetValidFileName(
-            aChannel, contentType, aOriginalURIWithExtension,
-            nsIMIMEService::VALIDATE_DEFAULT, newFileName))) {
-      nsCOMPtr<nsIFile> localFile;
-      GetLocalFileFromURI(aURI, getter_AddRefs(localFile));
-      if (localFile) {
-        localFile->SetLeafName(newFileName);
-
-        // Resync the URI with the file after the extension has been appended
-        return NS_MutateURI(aURI)
-            .Apply(&nsIFileURLMutator::SetFile, localFile)
+    nsCOMPtr<nsIMIMEInfo> mimeInfo;
+    mMIMEService->GetFromTypeAndExtension(contentType, ""_ns,
+                                          getter_AddRefs(mimeInfo));
+
+    nsCOMPtr<nsIFile> localFile;
+    GetLocalFileFromURI(aURI, getter_AddRefs(localFile));
+
+    if (mimeInfo) {
+      nsCOMPtr<nsIURL> url(do_QueryInterface(aURI));
+      NS_ENSURE_TRUE(url, NS_ERROR_FAILURE);
+
+      nsAutoCString newFileName;
+      url->GetFileName(newFileName);
+
+      // Test if the current extension is current for the mime type
+      bool hasExtension = false;
+      int32_t ext = newFileName.RFind(".");
+      if (ext != -1) {
+        mimeInfo->ExtensionExists(Substring(newFileName, ext + 1),
+                                  &hasExtension);
+      }
+
+      // Append the mime file extension
+      nsAutoCString fileExt;
+      if (!hasExtension) {
+        // Test if previous extension is acceptable
+        nsCOMPtr<nsIURL> oldurl(do_QueryInterface(aOriginalURIWithExtension));
+        NS_ENSURE_TRUE(oldurl, NS_ERROR_FAILURE);
+        oldurl->GetFileExtension(fileExt);
+        bool useOldExt = false;
+        if (!fileExt.IsEmpty()) {
+          mimeInfo->ExtensionExists(fileExt, &useOldExt);
+        }
+
+        // If the url doesn't have an extension, or we don't know the extension,
+        // try to use the primary extension for the type. If we don't know the
+        // primary extension for the type, just continue with the url extension.
+        if (!useOldExt) {
+          nsAutoCString primaryExt;
+          mimeInfo->GetPrimaryExtension(primaryExt);
+          if (!primaryExt.IsEmpty()) {
+            fileExt = primaryExt;
+          }
+        }
+
+        if (!fileExt.IsEmpty()) {
+          uint32_t newLength = newFileName.Length() + fileExt.Length() + 1;
+          if (newLength > kDefaultMaxFilenameLength) {
+            if (fileExt.Length() > kDefaultMaxFilenameLength / 2)
+              fileExt.Truncate(kDefaultMaxFilenameLength / 2);
+
+            uint32_t diff = kDefaultMaxFilenameLength - 1 - fileExt.Length();
+            if (newFileName.Length() > diff) newFileName.Truncate(diff);
+          }
+          newFileName.Append('.');
+          newFileName.Append(fileExt);
+        }
+
+        if (localFile) {
+          localFile->SetLeafName(NS_ConvertUTF8toUTF16(newFileName));
+
+          // Resync the URI with the file after the extension has been appended
+          return NS_MutateURI(url)
+              .Apply(&nsIFileURLMutator::SetFile, localFile)
+              .Finalize(aOutURI);
+        }
+        return NS_MutateURI(url)
+            .Apply(&nsIURLMutator::SetFileName, newFileName, nullptr)
             .Finalize(aOutURI);
       }
-      return NS_MutateURI(aURI)
-          .Apply(&nsIURLMutator::SetFileName,
-                 NS_ConvertUTF16toUTF8(newFileName), nullptr)
-          .Finalize(aOutURI);
     }
   }
 
   // TODO (:valentin) This method should always clone aURI
   aOutURI = aURI;
   return NS_OK;
 }
 
--- a/image/imgIRequest.idl
+++ b/image/imgIRequest.idl
@@ -96,24 +96,16 @@ interface imgIRequest : nsIRequest
    */
   readonly attribute nsIURI finalURI;
 
   readonly attribute imgINotificationObserver notificationObserver;
 
   readonly attribute string mimeType;
 
   /**
-   * The filename that should be used when saving the image. This is determined
-   * from the Content-Disposition, if present, or the uri of the image. This
-   * filename should be validated using nsIMIMEService::GetValidFilenameForSaving
-   * before creating the file.
-   */
-  readonly attribute ACString fileName;
-
-  /**
    * Clone this request; the returned request will have aObserver as the
    * observer.  aObserver will be notified synchronously (before the clone()
    * call returns) with all the notifications that have already been dispatched
    * for this image load.
    */
   imgIRequest clone(in imgINotificationObserver aObserver);
 
   /**
--- a/image/imgRequest.cpp
+++ b/image/imgRequest.cpp
@@ -26,25 +26,23 @@
 #include "nsIHttpChannel.h"
 #include "nsMimeTypes.h"
 
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsISupportsPrimitives.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsComponentManagerUtils.h"
 #include "nsContentUtils.h"
-#include "nsEscape.h"
 
 #include "plstr.h"   // PL_strcasestr(...)
 #include "prtime.h"  // for PR_Now
 #include "nsNetUtil.h"
 #include "nsIProtocolHandler.h"
 #include "imgIRequest.h"
 #include "nsProperties.h"
-#include "nsIURL.h"
 
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/SizeOfState.h"
 
 using namespace mozilla;
 using namespace mozilla::image;
 
 #define LOG_TEST(level) (MOZ_LOG_TEST(gImgLog, (level)))
@@ -458,40 +456,16 @@ bool imgRequest::HasConsumers() const {
 }
 
 already_AddRefed<image::Image> imgRequest::GetImage() const {
   MutexAutoLock lock(mMutex);
   RefPtr<image::Image> image = mImage;
   return image.forget();
 }
 
-void imgRequest::GetFileName(nsACString& aFileName) {
-  nsAutoString fileName;
-
-  nsCOMPtr<nsISupportsCString> supportscstr;
-  if (NS_SUCCEEDED(mProperties->Get("content-disposition",
-                                    NS_GET_IID(nsISupportsCString),
-                                    getter_AddRefs(supportscstr))) &&
-      supportscstr) {
-    nsAutoCString cdHeader;
-    supportscstr->GetData(cdHeader);
-    NS_GetFilenameFromDisposition(fileName, cdHeader);
-  }
-
-  if (fileName.IsEmpty()) {
-    nsCOMPtr<nsIURL> imgUrl(do_QueryInterface(mURI));
-    if (imgUrl) {
-      imgUrl->GetFileName(aFileName);
-      NS_UnescapeURL(aFileName);
-    }
-  } else {
-    aFileName = NS_ConvertUTF16toUTF8(fileName);
-  }
-}
-
 int32_t imgRequest::Priority() const {
   int32_t priority = nsISupportsPriority::PRIORITY_NORMAL;
   nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mRequest);
   if (p) {
     p->GetPriority(&priority);
   }
   return priority;
 }
--- a/image/imgRequest.h
+++ b/image/imgRequest.h
@@ -146,18 +146,16 @@ class imgRequest final : public nsIStrea
   bool IsChrome() const;
   bool IsData() const;
 
   nsresult GetImageErrorCode(void);
 
   /// Returns a non-owning pointer to this imgRequest's MIME type.
   const char* GetMimeType() const { return mContentType.get(); }
 
-  void GetFileName(nsACString& aFileName);
-
   /// @return the priority of the underlying network request, or
   /// PRIORITY_NORMAL if it doesn't support nsISupportsPriority.
   int32_t Priority() const;
 
   /// Adjust the priority of the underlying network request by @aDelta on behalf
   /// of @aProxy.
   void AdjustPriority(imgRequestProxy* aProxy, int32_t aDelta);
 
--- a/image/imgRequestProxy.cpp
+++ b/image/imgRequestProxy.cpp
@@ -742,26 +742,16 @@ imgRequestProxy::GetMimeType(char** aMim
     return NS_ERROR_FAILURE;
   }
 
   *aMimeType = NS_xstrdup(type);
 
   return NS_OK;
 }
 
-NS_IMETHODIMP
-imgRequestProxy::GetFileName(nsACString& aFileName) {
-  if (!GetOwner()) {
-    return NS_ERROR_FAILURE;
-  }
-
-  GetOwner()->GetFileName(aFileName);
-  return NS_OK;
-}
-
 imgRequestProxy* imgRequestProxy::NewClonedProxy() {
   return new imgRequestProxy();
 }
 
 NS_IMETHODIMP
 imgRequestProxy::Clone(imgINotificationObserver* aObserver,
                        imgIRequest** aClone) {
   nsresult result;
--- a/netwerk/mime/nsIMIMEService.idl
+++ b/netwerk/mime/nsIMIMEService.idl
@@ -3,17 +3,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIFile;
 interface nsIMIMEInfo;
 interface nsIURI;
-interface nsIChannel;
 
 %{C++
 #define NS_MIMESERVICE_CID                           \
 { /* 03af31da-3109-11d3-8cd0-0060b0fc14a3 */         \
     0x03af31da,                                      \
     0x3109,                                          \
     0x11d3,                                          \
     {0x8c, 0xd0, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3} \
@@ -93,129 +92,9 @@ interface nsIMIMEService : nsISupports {
      * @return                A nsIMIMEInfo object. This function must return
      *                        a MIMEInfo object if it can allocate one. The
      *                        only justifiable reason for not returning one is
      *                        an out-of-memory error.
      */
     nsIMIMEInfo getMIMEInfoFromOS(in ACString aType,
                                   in ACString aFileExtension,
                                   out boolean aFound);
-
-    /**
-     * Default filename validation for getValidFileName and
-     * validateFileNameForSaving where other flags are not true.
-     * That is, the extension is modified to fit the content type,
-     * duplicate whitespace is collapsed, and long filenames are
-     * truncated. A valid content type must be supplied. See the
-     * description of getValidFileName for more details about how
-     * the flags are used.
-     */
-    const long VALIDATE_DEFAULT = 0;
-
-    /**
-     * If true, then the filename is only validated to ensure that it is
-     * acceptable for the file system. If false, then the extension is also
-     * checked to ensure that it is valid for the content type. If the
-     * extension is not valid, the filename is modified to have the proper
-     * extension.
-     */
-    const long VALIDATE_SANITIZE_ONLY = 1;
-
-    /**
-     * Don't collapse strings of duplicate whitespace into a single string.
-     */
-    const long VALIDATE_DONT_COLLAPSE_WHITESPACE = 2;
-
-    /**
-     * Don't truncate long filenames.
-     */
-    const long VALIDATE_DONT_TRUNCATE = 4;
-
-    /**
-     * True to ignore the content type and guess the type from any existing
-     * extension instead. "application/octet-stream" is used as the default
-     * if there is no extension or there is no information available for
-     * the extension.
-     */
-    const long VALIDATE_GUESS_FROM_EXTENSION = 8;
-
-    /**
-     * If the filename is empty, return the empty filename
-     * without modification.
-     */
-    const long VALIDATE_ALLOW_EMPTY = 16;
-
-    /**
-     * Don't apply a default filename if the non-extension portion of the
-     * filename is empty.
-     */
-    const long VALIDATE_NO_DEFAULT_FILENAME = 32;
-
-    /**
-     * Generate a valid filename from the channel that can be used to save
-     * the content of the channel to the local disk.
-     *
-     * The filename is determined from the content disposition, the filename
-     * of the uri, or a default filename. The following modifications are
-     * applied:
-     *  - If the VALIDATE_SANITIZE_ONLY flag is not specified, then the
-     *    extension of the filename is modified to suit the supplied content type.
-     *  - Path separators (typically / and \) are replaced by underscores (_)
-     *  - Characters that are not valid or would be confusing in filenames are
-     *    replaced by spaces (*, :, etc)
-     *  - Bidi related marks are replaced by underscores (_)
-     *  - Whitespace and periods are removed from the beginning and end.
-     *  - Unless VALIDATE_DONT_COLLAPSE_WHITESPACE is specified, multiple
-     *    consecutive whitespace characters are collapsed to a single space
-     *    character, either ' ' or an ideographic space 0x3000 if present.
-     *  - Unless VALIDATE_DONT_TRUNCATE is specified, the filename is truncated
-     *    to a maximum length, preserving the extension if possible.
-     *  - Some filenames are invalid on certain platforms. These are replaced if
-     *    possible.
-     *
-     * If the VALIDATE_NO_DEFAULT_FILENAME flag is not specified, and after the
-     * rules above are applied, the resulting filename is empty, a default
-     * filename is used.
-     *
-     * If the VALIDATE_ALLOW_EMPTY flag is specified, an empty string may be
-     * returned only if the filename could not be determined or was blank.
-     *
-     * If either the VALIDATE_SANITIZE_ONLY or VALIDATE_GUESS_FROM_EXTENSION flags
-     * are specified, then the content type may be empty. Otherwise, the type must
-     * not be empty.
-     *
-     * The aOriginalURI would be specified if the channel is for a local file but
-     * it was originally sourced from a different uri.
-     *
-     * When saving an image, use validateFileNameForSaving instead and
-     * pass the result of imgIRequest::GetFileName() as the filename to
-     * check.
-     *
-     * @param aChannel The channel of the content to save.
-     * @param aType The MIME type to use, which would usually be the
-     *              same as the content type of the channel.
-     * @param aOriginalURL the source url of the file, but may be null.
-     * @param aFlags one or more of the flags above.
-     * @returns The resulting filename.
-     */
-    AString getValidFileName(in nsIChannel aChannel,
-                             in ACString aType,
-                             in nsIURI aOriginalURI,
-                             in unsigned long aFlags);
-
-    /**
-     * Similar to getValidFileName, but used when a specific filename needs
-     * to be validated. The filename is modified as needed based on the 
-     * content type in the same manner as getValidFileName.
-     *
-     * If the filename came from a uri, it should not be escaped, that is,
-     * any needed unescaping of the filename should happen before calling
-     * this method.
-     *
-     * @param aType The MIME type to use.
-     * @param aFlags one or more of the flags above.
-     * @param aFileName The filename to validate.
-     * @returns The validated filename.
-     */
-    AString validateFileNameForSaving(in AString aFileName,
-                                      in ACString aType,
-                                      in unsigned long aFlags);
 };
--- a/toolkit/components/downloads/DownloadPaths.jsm
+++ b/toolkit/components/downloads/DownloadPaths.jsm
@@ -5,32 +5,93 @@
 /**
  * Provides methods for giving names and paths to files being downloaded.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["DownloadPaths"];
 
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "AppConstants",
+  "resource://gre/modules/AppConstants.jsm"
+);
+
+/**
+ * Platform-dependent regular expression used by the "sanitize" method.
+ */
+XPCOMUtils.defineLazyGetter(this, "gConvertToSpaceRegExp", () => {
+  // Note: we remove colons everywhere to avoid issues in subresource URL
+  // parsing, as well as filename restrictions on some OSes (see bug 1562176).
+  /* eslint-disable no-control-regex */
+  switch (AppConstants.platform) {
+    // On mobile devices, the file system may be very limited in what it
+    // considers valid characters. To avoid errors, sanitize conservatively.
+    case "android":
+      return /[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g;
+    case "win":
+      return /[\x00-\x1f\x7f-\x9f:*?|]+/g;
+    default:
+      return /[\x00-\x1f\x7f-\x9f:]+/g;
+  }
+  /* eslint-enable no-control-regex */
+});
+
 var DownloadPaths = {
   /**
-   * Sanitizes an arbitrary string via mimeSvc.validateFileNameForSaving.
+   * Sanitizes an arbitrary string for use as the local file name of a download.
+   * The input is often a document title or a manually edited name. The output
+   * can be an empty string if the input does not include any valid character.
+   *
+   * The length of the resulting string is not limited, because restrictions
+   * apply to the full path name after the target folder has been added.
+   *
+   * Splitting the base name and extension to add a counter or to identify the
+   * file type should only be done after the sanitization process, because it
+   * can alter the final part of the string or remove leading dots.
+   *
+   * Runs of slashes and backslashes are replaced with an underscore.
+   *
+   * On Windows, the angular brackets `<` and `>` are replaced with parentheses,
+   * and double quotes are replaced with single quotes.
+   *
+   * Runs of control characters are replaced with a space. On Mac, colons are
+   * also included in this group. On Windows, stars, question marks, and pipes
+   * are additionally included. On Android, semicolons, commas, plus signs,
+   * equal signs, and brackets are additionally included.
+   *
+   * Leading and trailing dots and whitespace are removed on all platforms. This
+   * avoids the accidental creation of hidden files on Unix and invalid or
+   * inaccessible file names on Windows. These characters are not removed when
+   * located at the end of the base name or at the beginning of the extension.
    *
    * @param {string} leafName The full leaf name to sanitize
    * @param {boolean} [compressWhitespaces] Whether consecutive whitespaces
    *        should be compressed.
    */
   sanitize(leafName, { compressWhitespaces = true } = {}) {
-    const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
-
-    let flags = mimeSvc.VALIDATE_SANITIZE_ONLY | mimeSvc.VALIDATE_DONT_TRUNCATE;
-    if (!compressWhitespaces) {
-      flags |= mimeSvc.VALIDATE_DONT_COLLAPSE_WHITESPACE;
+    if (AppConstants.platform == "win") {
+      leafName = leafName
+        .replace(/</g, "(")
+        .replace(/>/g, ")")
+        .replace(/"/g, "'");
     }
-    return mimeSvc.validateFileNameForSaving(leafName, "", flags);
+    leafName = leafName
+      .replace(/[\\/]+/g, "_")
+      .replace(/[\u200e\u200f\u202a-\u202e]/g, "")
+      .replace(gConvertToSpaceRegExp, " ");
+    if (compressWhitespaces) {
+      leafName = leafName.replace(/\s{2,}/g, " ");
+    }
+    return leafName.replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
   },
 
   /**
    * Creates a uniquely-named file starting from the name of the provided file.
    * If a file with the provided name already exists, the function attempts to
    * create nice alternatives, like "base(1).ext" (instead of "base-1.ext").
    *
    * If a unique name cannot be found, the function throws the XPCOM exception
--- a/toolkit/components/downloads/test/unit/test_DownloadPaths.js
+++ b/toolkit/components/downloads/test/unit/test_DownloadPaths.js
@@ -35,55 +35,55 @@ add_task(async function test_sanitize() 
   const kSpecialChars = 'A:*?|""<<>>;,+=[]B][=+,;>><<""|?*:C';
   if (AppConstants.platform == "android") {
     testSanitize(kSpecialChars, "A B C");
     testSanitize(" :: Website :: ", "Website");
     testSanitize("* Website!", "Website!");
     testSanitize("Website | Page!", "Website Page!");
     testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
   } else if (AppConstants.platform == "win") {
-    testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C");
+    testSanitize(kSpecialChars, "A ''(());,+=[]B][=+,;))(('' C");
     testSanitize(" :: Website :: ", "Website");
     testSanitize("* Website!", "Website!");
     testSanitize("Website | Page!", "Website Page!");
     testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
   } else if (AppConstants.platform == "macosx") {
-    testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C");
+    testSanitize(kSpecialChars, 'A *?|""<<>>;,+=[]B][=+,;>><<""|?* C');
     testSanitize(" :: Website :: ", "Website");
-    testSanitize("* Website!", "Website!");
-    testSanitize("Website | Page!", "Website Page!");
+    testSanitize("* Website!", "* Website!");
+    testSanitize("Website | Page!", "Website | Page!");
     testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
   } else {
-    testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C");
+    testSanitize(kSpecialChars, kSpecialChars.replace(/[:]/g, " "));
     testSanitize(" :: Website :: ", "Website");
-    testSanitize("* Website!", "Website!");
-    testSanitize("Website | Page!", "Website Page!");
+    testSanitize("* Website!", "* Website!");
+    testSanitize("Website | Page!", "Website | Page!");
     testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
   }
 
   // Conversion of consecutive runs of slashes and backslashes to underscores.
-  testSanitize("\\ \\\\Website\\/Page// /", "_ __Website__Page__ _");
+  testSanitize("\\ \\\\Website\\/Page// /", "_ _Website_Page_ _");
 
   // Removal of leading and trailing whitespace and dots after conversion.
   testSanitize("  Website  ", "Website");
   testSanitize(". . Website . Page . .", "Website . Page");
   testSanitize(" File . txt ", "File . txt");
   testSanitize("\f\n\r\t\v\x00\x1f\x7f\x80\x9f\xa0 . txt", "txt");
   testSanitize("\u1680\u180e\u2000\u2008\u200a . txt", "txt");
   testSanitize("\u2028\u2029\u202f\u205f\u3000\ufeff . txt", "txt");
 
   // Strings with whitespace and dots only.
   testSanitize(".", "");
   testSanitize("..", "");
   testSanitize(" ", "");
   testSanitize(" . ", "");
 
   // Stripping of BIDI formatting characters.
-  testSanitize("\u200e \u202b\u202c\u202d\u202etest\x7f\u200f", "_ ____test _");
-  testSanitize("AB\x7f\u202a\x7f\u202a\x7fCD", "AB _ _ CD");
+  testSanitize("\u200e \u202b\u202c\u202d\u202etest\x7f\u200f", "test");
+  testSanitize("AB\x7f\u202a\x7f\u202a\x7fCD", "AB CD");
 
   // Stripping of colons:
   testSanitize("foo:bar", "foo bar");
 
   // not compressing whitespaces.
   testSanitize("foo : bar", "foo   bar", { compressWhitespaces: false });
 });
 
--- a/toolkit/content/contentAreaUtils.js
+++ b/toolkit/content/contentAreaUtils.js
@@ -563,57 +563,47 @@ function initFileInfo(
   aFI,
   aURL,
   aURLCharset,
   aDocument,
   aContentType,
   aContentDisposition
 ) {
   try {
-    let uriExt = null;
     // Get an nsIURI object from aURL if possible:
     try {
       aFI.uri = makeURI(aURL, aURLCharset);
       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
       // populate extra object fields (eg filename and file extension).
-      uriExt = aFI.uri.QueryInterface(Ci.nsIURL).fileExtension;
+      var url = aFI.uri.QueryInterface(Ci.nsIURL);
+      aFI.fileExt = url.fileExtension;
     } catch (e) {}
 
     // Get the default filename:
-    let fileName = getDefaultFileName(
+    aFI.fileName = getDefaultFileName(
       aFI.suggestedFileName || aFI.fileName,
       aFI.uri,
       aDocument,
       aContentDisposition
     );
-
-    let mimeService = this.getMIMEService();
-    aFI.fileName = mimeService.validateFileNameForSaving(
-      fileName,
-      aContentType,
-      mimeService.VALIDATE_DEFAULT
-    );
-
-    // If uriExt is blank, consider: aFI.suggestedFileName is supplied if
-    // saveURL(...) was the original caller (hence both aContentType and
+    // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
+    // if saveURL(...) was the original caller (hence both aContentType and
     // aDocument are blank). If they were saving a link to a website then make
     // the extension .htm .
     if (
-      !uriExt &&
+      !aFI.fileExt &&
       !aDocument &&
       !aContentType &&
       /^http(s?):\/\//i.test(aURL)
     ) {
       aFI.fileExt = "htm";
       aFI.fileBaseName = aFI.fileName;
     } else {
-      let idx = aFI.fileName.lastIndexOf(".");
-      aFI.fileBaseName =
-        idx >= 0 ? aFI.fileName.substring(0, idx) : aFI.fileName;
-      aFI.fileExt = idx >= 0 ? aFI.fileName.substring(idx + 1) : null;
+      aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
+      aFI.fileBaseName = getFileBaseName(aFI.fileName);
     }
   } catch (e) {}
 }
 
 /**
  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
  * prompting the user to confirm (or change) the fileName.
  * @param aFpP
@@ -650,17 +640,19 @@ function promiseTargetFile(
 
     // Default to the user's default downloads directory configured
     // through download prefs.
     let dirPath = await Downloads.getPreferredDownloadsDirectory();
     let dirExists = await OS.File.exists(dirPath);
     let dir = new FileUtils.File(dirPath);
 
     if (useDownloadDir && dirExists) {
-      dir.append(aFpP.fileInfo.fileName);
+      dir.append(
+        getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt)
+      );
       aFpP.file = uniqueFile(dir);
       return true;
     }
 
     // We must prompt for the file name explicitly.
     // If we must prompt because we were asked to...
     let file = await new Promise(resolve => {
       if (useDownloadDir) {
@@ -691,17 +683,20 @@ function promiseTargetFile(
     fp.init(
       window,
       ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
       Ci.nsIFilePicker.modeSave
     );
 
     fp.displayDirectory = dir;
     fp.defaultExtension = aFpP.fileInfo.fileExt;
-    fp.defaultString = aFpP.fileInfo.fileName;
+    fp.defaultString = getNormalizedLeafName(
+      aFpP.fileInfo.fileName,
+      aFpP.fileInfo.fileExt
+    );
     appendFiltersForContentType(
       fp,
       aFpP.contentType,
       aFpP.fileInfo.fileExt,
       aFpP.saveMode
     );
 
     // The index of the selected filter is only preserved and restored if there's
@@ -1021,76 +1016,94 @@ function getDefaultFileName(
           "name",
           charset,
           true,
           dummy
         );
       } catch (e) {}
     }
     if (fileName) {
-      return Services.textToSubURI.unEscapeURIForUI(
-        fileName,
-        /* dontEscape = */ true
+      return validateFileName(
+        Services.textToSubURI.unEscapeURIForUI(
+          fileName,
+          /* dontEscape = */ true
+        )
       );
     }
   }
 
   let docTitle;
   if (aDocument && aDocument.title && aDocument.title.trim()) {
     // If the document looks like HTML or XML, try to use its original title.
+    docTitle = validateFileName(aDocument.title);
     let contentType = aDocument.contentType;
     if (
       contentType == "application/xhtml+xml" ||
       contentType == "application/xml" ||
       contentType == "image/svg+xml" ||
       contentType == "text/html" ||
       contentType == "text/xml"
     ) {
       // 2) Use the document title
-      return aDocument.title;
+      return docTitle;
     }
   }
 
   try {
     var url = aURI.QueryInterface(Ci.nsIURL);
     if (url.fileName != "") {
       // 3) Use the actual file name, if present
-      return Services.textToSubURI.unEscapeURIForUI(
-        url.fileName,
-        /* dontEscape = */ true
+      return validateFileName(
+        Services.textToSubURI.unEscapeURIForUI(
+          url.fileName,
+          /* dontEscape = */ true
+        )
       );
     }
   } catch (e) {
     // This is something like a data: and so forth URI... no filename here.
   }
 
   // Don't use the title if it's from a data URI
   if (docTitle && aURI?.scheme != "data") {
     // 4) Use the document title
     return docTitle;
   }
 
   if (aDefaultFileName) {
     // 5) Use the caller-provided name, if any
-    return aDefaultFileName;
+    return validateFileName(aDefaultFileName);
+  }
+
+  // 6) If this is a directory, use the last directory name
+  var path = aURI.pathQueryRef.match(/\/([^\/]+)\/$/);
+  if (path && path.length > 1) {
+    return validateFileName(path[1]);
   }
 
   try {
     if (aURI.host) {
-      // 6) Use the host.
-      return aURI.host;
+      // 7) Use the host.
+      return validateFileName(aURI.host);
     }
   } catch (e) {
     // Some files have no information at all, like Javascript generated pages
   }
-
-  return "";
+  try {
+    // 8) Use the default file name
+    return ContentAreaUtils.stringBundle.GetStringFromName(
+      "DefaultSaveFileName"
+    );
+  } catch (e) {
+    // in case localized string cannot be found
+  }
+  // 9) If all else fails, use "index"
+  return "index";
 }
 
-// This is only used after the user has entered a filename.
 function validateFileName(aFileName) {
   let processed = DownloadPaths.sanitize(aFileName) || "_";
   if (AppConstants.platform == "android") {
     // If a large part of the filename has been sanitized, then we
     // will use a default filename instead
     if (processed.replace(/_/g, "").length <= processed.length / 2) {
       // We purposefully do not use a localized default filename,
       // which we could have done using
@@ -1106,16 +1119,130 @@ function validateFileName(aFileName) {
           processed += "." + suffix;
         }
       }
     }
   }
   return processed;
 }
 
+// This is the set of image extensions supported by extraMimeEntries in
+// nsExternalHelperAppService.
+const kImageExtensions = new Set([
+  "art",
+  "bmp",
+  "gif",
+  "ico",
+  "cur",
+  "jpeg",
+  "jpg",
+  "jfif",
+  "pjpeg",
+  "pjp",
+  "png",
+  "apng",
+  "tiff",
+  "tif",
+  "xbm",
+  "svg",
+  "webp",
+  "avif",
+  "jxl",
+]);
+
+function getNormalizedLeafName(aFile, aDefaultExtension) {
+  if (!aDefaultExtension) {
+    return aFile;
+  }
+
+  if (AppConstants.platform == "win") {
+    // Remove trailing dots and spaces on windows
+    aFile = aFile.replace(/[\s.]+$/, "");
+  }
+
+  // Remove leading dots
+  aFile = aFile.replace(/^\.+/, "");
+
+  // Include the default extension in the file name to which we're saving.
+  var i = aFile.lastIndexOf(".");
+  let previousExtension = aFile.substr(i + 1).toLowerCase();
+  if (previousExtension != aDefaultExtension.toLowerCase()) {
+    // Suffixing the extension is the safe bet - it preserves the previous
+    // extension in case that's meaningful (e.g. various text files served
+    // with text/plain will end up as `foo.cpp.txt`, in the worst case).
+    // However, for images, the extension derived from the URL should be
+    // replaced if the content type indicates a different filetype - this makes
+    // sure that we treat e.g. feature-tested webp/avif images correctly.
+    if (kImageExtensions.has(previousExtension)) {
+      return aFile.substr(0, i + 1) + aDefaultExtension;
+    }
+    return aFile + "." + aDefaultExtension;
+  }
+
+  return aFile;
+}
+
+function getDefaultExtension(aFilename, aURI, aContentType) {
+  if (
+    aContentType == "text/plain" ||
+    aContentType == "application/octet-stream" ||
+    aURI.scheme == "ftp"
+  ) {
+    return "";
+  } // temporary fix for bug 120327
+
+  // First try the extension from the filename
+  var url = Cc["@mozilla.org/network/standard-url-mutator;1"]
+    .createInstance(Ci.nsIURIMutator)
+    .setSpec("http://example.com") // construct the URL
+    .setFilePath(aFilename)
+    .finalize()
+    .QueryInterface(Ci.nsIURL);
+
+  var ext = url.fileExtension;
+
+  // This mirrors some code in nsExternalHelperAppService::DoContent
+  // Use the filename first and then the URI if that fails
+
+  // For images, rely solely on the mime type if known.
+  // All the extension is going to do is lie to us.
+  var lookupExt = ext;
+  if (aContentType?.startsWith("image/")) {
+    lookupExt = "";
+  }
+  var mimeInfo = getMIMEInfoForType(aContentType, lookupExt);
+
+  if (ext && mimeInfo && mimeInfo.extensionExists(ext)) {
+    return ext;
+  }
+
+  // Well, that failed.  Now try the extension from the URI
+  var urlext;
+  try {
+    url = aURI.QueryInterface(Ci.nsIURL);
+    urlext = url.fileExtension;
+  } catch (e) {}
+
+  if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
+    return urlext;
+  }
+
+  // That failed as well. If we could lookup the MIME use the primary
+  // extension for that type.
+  try {
+    if (mimeInfo) {
+      return mimeInfo.primaryExtension;
+    }
+  } catch (e) {}
+
+  // Fall back on the extensions in the filename and URI for lack
+  // of anything better.
+  return ext || urlext;
+}
+
 function GetSaveModeForContentType(aContentType, aDocument) {
   // We can only save a complete page if we have a loaded document,
   if (!aDocument) {
     return SAVEMODE_FILEONLY;
   }
 
   // Find the possible save modes using the provided content type
   var saveMode = SAVEMODE_FILEONLY;
--- a/toolkit/mozapps/downloads/HelperAppDlg.jsm
+++ b/toolkit/mozapps/downloads/HelperAppDlg.jsm
@@ -289,23 +289,21 @@ nsUnknownContentTypeDialog.prototype = {
         );
 
         if (autodownload) {
           // Retrieve the user's default download directory
           let preferredDir = await Downloads.getPreferredDownloadsDirectory();
           let defaultFolder = new FileUtils.File(preferredDir);
 
           try {
-            if (aDefaultFileName) {
-              result = this.validateLeafName(
-                defaultFolder,
-                aDefaultFileName,
-                aSuggestedFileExtension
-              );
-            }
+            result = this.validateLeafName(
+              defaultFolder,
+              aDefaultFileName,
+              aSuggestedFileExtension
+            );
           } catch (ex) {
             // When the default download directory is write-protected,
             // prompt the user for a different target file.
           }
 
           // Check to make sure we have a valid directory, otherwise, prompt
           if (result) {
             // This path is taken when we have a writable default download directory.
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -46,18 +46,16 @@
 #include "nsDirectoryServiceDefs.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsThreadUtils.h"
 #include "nsIMutableArray.h"
 #include "nsIRedirectHistoryEntry.h"
 #include "nsOSHelperAppService.h"
 #include "nsOSHelperAppServiceChild.h"
 #include "nsContentSecurityUtils.h"
-#include "nsUTF8Utils.h"
-#include "nsUnicodeProperties.h"
 
 // used to access our datastore of user-configured helper applications
 #include "nsIHandlerService.h"
 #include "nsIMIMEInfo.h"
 #include "nsIHelperAppLauncherDialog.h"
 #include "nsIContentDispatchChooser.h"
 #include "nsNetUtil.h"
 #include "nsIPrivateBrowsingChannel.h"
@@ -100,30 +98,27 @@
 #include "nsXULAppAPI.h"
 #include "nsPIDOMWindow.h"
 #include "ExternalHelperAppChild.h"
 
 #include "mozilla/dom/nsHTTPSOnlyUtils.h"
 
 #ifdef XP_WIN
 #  include "nsWindowsHelpers.h"
-#  include "nsLocalFile.h"
 #endif
 
 #include "mozilla/Components.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ipc/URIUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::ipc;
 using namespace mozilla::dom;
 
-#define kDefaultMaxFileNameLength 255
-
 // Download Folder location constants
 #define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
 #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
 enum {
   NS_FOLDER_VALUE_DESKTOP = 0,
   NS_FOLDER_VALUE_DOWNLOADS = 1,
   NS_FOLDER_VALUE_CUSTOM = 2
 };
@@ -180,16 +175,118 @@ static nsresult UnescapeFragment(const n
                                  nsACString& aResult) {
   nsAutoString result;
   nsresult rv = UnescapeFragment(aFragment, aURI, result);
   if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
   return rv;
 }
 
 /**
+ * Given a channel, returns the filename and extension the channel has.
+ * This uses the URL and other sources (nsIMultiPartChannel).
+ * Also gives back whether the channel requested external handling (i.e.
+ * whether Content-Disposition: attachment was sent)
+ * @param aChannel The channel to extract the filename/extension from
+ * @param aFileName [out] Reference to the string where the filename should be
+ *        stored. Empty if it could not be retrieved.
+ *        WARNING - this filename may contain characters which the OS does not
+ *        allow as part of filenames!
+ * @param aExtension [out] Reference to the string where the extension should
+ *        be stored. Empty if it could not be retrieved. Stored in UTF-8.
+ * @param aAllowURLExtension (optional) Get the extension from the URL if no
+ *        Content-Disposition header is present. Default is true.
+ * @retval true The server sent Content-Disposition:attachment or equivalent
+ * @retval false Content-Disposition: inline or no content-disposition header
+ *         was sent.
+ */
+static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel,
+                                               nsString& aFileName,
+                                               nsCString& aExtension,
+                                               bool aAllowURLExtension = true) {
+  aExtension.Truncate();
+  /*
+   * If the channel is an http or part of a multipart channel and we
+   * have a content disposition header set, then use the file name
+   * suggested there as the preferred file name to SUGGEST to the
+   * user.  we shouldn't actually use that without their
+   * permission... otherwise just use our temp file
+   */
+  bool handleExternally = false;
+  uint32_t disp;
+  nsresult rv = aChannel->GetContentDisposition(&disp);
+  bool gotFileNameFromURI = false;
+  if (NS_SUCCEEDED(rv)) {
+    aChannel->GetContentDispositionFilename(aFileName);
+    if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true;
+  }
+
+  // If the disposition header didn't work, try the filename from nsIURL
+  nsCOMPtr<nsIURI> uri;
+  aChannel->GetURI(getter_AddRefs(uri));
+  nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
+  if (url && aFileName.IsEmpty()) {
+    if (aAllowURLExtension) {
+      url->GetFileExtension(aExtension);
+      UnescapeFragment(aExtension, url, aExtension);
+
+      // Windows ignores terminating dots. So we have to as well, so
+      // that our security checks do "the right thing"
+      // In case the aExtension consisted only of the dot, the code below will
+      // extract an aExtension from the filename
+      aExtension.Trim(".", false);
+    }
+
+    // try to extract the file name from the url and use that as a first pass as
+    // the leaf name of our temp file...
+    nsAutoCString leafName;
+    url->GetFileName(leafName);
+    if (!leafName.IsEmpty()) {
+      gotFileNameFromURI = true;
+      rv = UnescapeFragment(leafName, url, aFileName);
+      if (NS_FAILED(rv)) {
+        CopyUTF8toUTF16(leafName, aFileName);  // use escaped name
+      }
+    }
+  }
+
+  // If we have a filename and no extension, remove trailing dots from the
+  // filename and extract the extension if that is possible.
+  if (aExtension.IsEmpty() && !aFileName.IsEmpty()) {
+    // Windows ignores terminating dots. So we have to as well, so
+    // that our security checks do "the right thing"
+    aFileName.Trim(".", false);
+    // We can get an extension if the filename is from a header, or if getting
+    // it from the URL was allowed.
+    bool canGetExtensionFromFilename =
+        !gotFileNameFromURI || aAllowURLExtension;
+    // ... , or if the mimetype is meaningless and we have nothing to go on:
+    if (!canGetExtensionFromFilename) {
+      nsAutoCString contentType;
+      if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) {
+        canGetExtensionFromFilename =
+            contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
+            contentType.EqualsIgnoreCase("binary/octet-stream") ||
+            contentType.EqualsIgnoreCase("application/x-msdownload");
+      }
+    }
+
+    if (canGetExtensionFromFilename) {
+      // XXX RFindCharInReadable!!
+      nsAutoString fileNameStr(aFileName);
+      int32_t idx = fileNameStr.RFindChar(char16_t('.'));
+      if (idx != kNotFound)
+        CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1),
+                        aExtension);
+    }
+  }
+
+  return handleExternally;
+}
+
+/**
  * Obtains the directory to use.  This tends to vary per platform, and
  * needs to be consistent throughout our codepaths. For platforms where
  * helper apps use the downloads directory, this should be kept in
  * sync with DownloadIntegration.jsm.
  *
  * Optionally skip availability of the directory and storage.
  */
 static nsresult GetDownloadDirectory(nsIFile** _directory,
@@ -510,16 +607,18 @@ static const nsExtraMimeTypeEntry extraM
     {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "docx", "Microsoft Word (Open XML)"},
     {"application/"
      "vnd.openxmlformats-officedocument.presentationml.presentation",
      "pptx", "Microsoft PowerPoint (Open XML)"},
     {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "xlsx", "Microsoft Excel (Open XML)"},
 
+    // Note: if you add new image types, please also update the list in
+    // contentAreaUtils.js to match.
     {IMAGE_ART, "art", "ART Image"},
     {IMAGE_BMP, "bmp", "BMP Image"},
     {IMAGE_GIF, "gif", "GIF Image"},
     {IMAGE_ICO, "ico,cur", "ICO Image"},
     {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
     {IMAGE_PNG, "png", "PNG Image"},
     {IMAGE_APNG, "apng", "APNG Image"},
     {IMAGE_TIFF, "tiff,tif", "TIFF Image"},
@@ -705,20 +804,18 @@ nsresult nsExternalHelperAppService::DoC
       childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
       contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
       referrer, aContentContext, shouldCloseWindow));
 
   NS_ADDREF(*aStreamListener = childListener);
 
   uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
 
-  SanitizeFileName(fileName, EmptyCString(), 0);
-
   RefPtr<nsExternalAppHandler> handler =
-      new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
+      new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext,
                                this, fileName, reason, aForceSave);
   if (!handler) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   childListener->SetHandler(handler);
   return NS_OK;
 }
@@ -728,73 +825,126 @@ NS_IMETHODIMP nsExternalHelperAppService
     BrowsingContext* aContentContext, bool aForceSave,
     nsIInterfaceRequestor* aWindowContext,
     nsIStreamListener** aStreamListener) {
   MOZ_ASSERT(!XRE_IsContentProcess());
 
   nsAutoString fileName;
   nsAutoCString fileExtension;
   uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
-
+  uint32_t contentDisposition = -1;
+
+  // Get the file extension and name that we will need later
   nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+  nsCOMPtr<nsIURI> uri;
+  int64_t contentLength = -1;
   if (channel) {
-    uint32_t contentDisposition = -1;
+    channel->GetURI(getter_AddRefs(uri));
+    channel->GetContentLength(&contentLength);
     channel->GetContentDisposition(&contentDisposition);
-    if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
+    channel->GetContentDispositionFilename(fileName);
+
+    // Check if we have a POST request, in which case we don't want to use
+    // the url's extension
+    bool allowURLExt = !net::ChannelIsPost(channel);
+
+    // Check if we had a query string - we don't want to check the URL
+    // extension if a query is present in the URI
+    // If we already know we don't want to check the URL extension, don't
+    // bother checking the query
+    if (uri && allowURLExt) {
+      nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
+
+      if (url) {
+        nsAutoCString query;
+
+        // We only care about the query for HTTP and HTTPS URLs
+        if (uri->SchemeIs("http") || uri->SchemeIs("https")) {
+          url->GetQuery(query);
+        }
+
+        // Only get the extension if the query is empty; if it isn't, then the
+        // extension likely belongs to a cgi script and isn't helpful
+        allowURLExt = query.IsEmpty();
+      }
+    }
+    // Extract name & extension
+    bool isAttachment = GetFilenameAndExtensionFromChannel(
+        channel, fileName, fileExtension, allowURLExt);
+    LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)",
+         fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(),
+         isAttachment));
+    if (isAttachment) {
       reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
     }
   }
 
-  *aStreamListener = nullptr;
-
-  // Get the file extension and name that we will need later
-  nsCOMPtr<nsIURI> uri;
-  bool allowURLExtension =
-      GetFileNameFromChannel(channel, fileName, getter_AddRefs(uri));
-
-  uint32_t flags = VALIDATE_ALLOW_EMPTY;
+  LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n",
+       PromiseFlatCString(aMimeContentType).get(), fileExtension.get()));
+
+  // We get the mime service here even though we're the default implementation
+  // of it, so it's possible to override only the mime service and not need to
+  // reimplement the whole external helper app service itself.
+  nsCOMPtr<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID));
+  NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE);
+
+  // Try to find a mime object by looking at the mime type/extension
+  nsCOMPtr<nsIMIMEInfo> mimeInfo;
   if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
                               nsCaseInsensitiveCStringComparator)) {
-    flags |= VALIDATE_GUESS_FROM_EXTENSION;
+    nsAutoCString mimeType;
+    if (!fileExtension.IsEmpty()) {
+      mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension,
+                                       getter_AddRefs(mimeInfo));
+      if (mimeInfo) {
+        mimeInfo->GetMIMEType(mimeType);
+
+        LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(),
+             fileExtension.get()));
+      }
+    }
+
+    if (fileExtension.IsEmpty() || mimeType.IsEmpty()) {
+      // Extension lookup gave us no useful match
+      mimeSvc->GetFromTypeAndExtension(
+          nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension,
+          getter_AddRefs(mimeInfo));
+      mimeType.AssignLiteral(APPLICATION_OCTET_STREAM);
+    }
+
+    if (channel) {
+      channel->SetContentType(mimeType);
+    }
+
+    // Don't overwrite SERVERREQUEST
+    if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
+      reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
+    }
+  } else {
+    mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension,
+                                     getter_AddRefs(mimeInfo));
   }
-
-  nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
-      fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension);
-
   LOG(("Type/Ext lookup found 0x%p\n", mimeInfo.get()));
 
   // No mimeinfo -> we can't continue. probably OOM.
   if (!mimeInfo) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
-  if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
-    if (channel) {
-      // Replace the content type with what was guessed.
-      nsAutoCString mimeType;
-      mimeInfo->GetMIMEType(mimeType);
-      channel->SetContentType(mimeType);
-    }
-
-    if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
-      reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
-    }
-  }
-
-  nsAutoString extension;
-  int32_t dotidx = fileName.RFind(".");
-  if (dotidx != -1) {
-    extension = Substring(fileName, dotidx + 1);
-  }
+  *aStreamListener = nullptr;
+  // We want the mimeInfo's primary extension to pass it to
+  // nsExternalAppHandler
+  nsAutoCString buf;
+  mimeInfo->GetPrimaryExtension(buf);
 
   // NB: ExternalHelperAppParent depends on this listener always being an
   // nsExternalAppHandler. If this changes, make sure to update that code.
-  nsExternalAppHandler* handler = new nsExternalAppHandler(
-      mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
-      reason, aForceSave);
+  nsExternalAppHandler* handler =
+      new nsExternalAppHandler(mimeInfo, buf, aContentContext, aWindowContext,
+                               this, fileName, reason, aForceSave);
   if (!handler) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   NS_ADDREF(*aStreamListener = handler);
   return NS_OK;
 }
 
@@ -1273,24 +1423,24 @@ NS_INTERFACE_MAP_BEGIN(nsExternalAppHand
   NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
   NS_INTERFACE_MAP_ENTRY(nsICancelable)
   NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
   NS_INTERFACE_MAP_ENTRY(nsINamed)
   NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
 NS_INTERFACE_MAP_END
 
 nsExternalAppHandler::nsExternalAppHandler(
-    nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
+    nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension,
     BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
     nsExternalHelperAppService* aExtProtSvc,
-    const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave)
+    const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
     : mMimeInfo(aMIMEInfo),
       mBrowsingContext(aBrowsingContext),
       mWindowContext(aWindowContext),
-      mSuggestedFileName(aSuggestedFileName),
+      mSuggestedFileName(aSuggestedFilename),
       mForceSave(aForceSave),
       mCanceled(false),
       mStopRequestIssued(false),
       mIsFileChannel(false),
       mShouldCloseWindow(false),
       mHandleInternally(false),
       mReason(aReason),
       mTempFileIsExecutable(false),
@@ -1298,28 +1448,145 @@ nsExternalAppHandler::nsExternalAppHandl
       mContentLength(-1),
       mProgress(0),
       mSaver(nullptr),
       mDialogProgressListener(nullptr),
       mTransfer(nullptr),
       mRequest(nullptr),
       mExtProtSvc(aExtProtSvc) {
   // make sure the extention includes the '.'
-  if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') {
-    mFileExtension = char16_t('.');
+  if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.')
+    mTempFileExtension = char16_t('.');
+  AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension);
+
+  // Get mSuggestedFileName's current file extension.
+  nsAutoString originalFileExt;
+  int32_t pos = mSuggestedFileName.RFindChar('.');
+  if (pos != kNotFound) {
+    mSuggestedFileName.Right(originalFileExt,
+                             mSuggestedFileName.Length() - pos);
   }
-  mFileExtension.Append(aFileExtension);
+
+  // replace platform specific path separator and illegal characters to avoid
+  // any confusion.
+  // Try to keep the use of spaces or underscores in sync with the Downloads
+  // code sanitization in DownloadPaths.jsm
+  mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
+  mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
+  mSuggestedFileName.ReplaceChar(char16_t(0), '_');
+  mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
+  mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
+
+  // Remove unsafe bidi characters which might have spoofing implications (bug
+  // 511521).
+  const char16_t unsafeBidiCharacters[] = {
+      char16_t(0x061c),  // Arabic Letter Mark
+      char16_t(0x200e),  // Left-to-Right Mark
+      char16_t(0x200f),  // Right-to-Left Mark
+      char16_t(0x202a),  // Left-to-Right Embedding
+      char16_t(0x202b),  // Right-to-Left Embedding
+      char16_t(0x202c),  // Pop Directional Formatting
+      char16_t(0x202d),  // Left-to-Right Override
+      char16_t(0x202e),  // Right-to-Left Override
+      char16_t(0x2066),  // Left-to-Right Isolate
+      char16_t(0x2067),  // Right-to-Left Isolate
+      char16_t(0x2068),  // First Strong Isolate
+      char16_t(0x2069),  // Pop Directional Isolate
+      char16_t(0)};
+  mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_');
+  mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_');
+
+  // Remove trailing or leading spaces that we may have generated while
+  // sanitizing.
+  mSuggestedFileName.CompressWhitespace();
+  mTempFileExtension.CompressWhitespace();
+
+  EnsureCorrectExtension(originalFileExt);
 
   mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
 }
 
 nsExternalAppHandler::~nsExternalAppHandler() {
   MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
 }
 
+bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) {
+  nsAutoCString MIMEType;
+  if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) {
+    return false;
+  }
+
+  bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
+                  StringBeginsWith(MIMEType, "audio/"_ns) ||
+                  StringBeginsWith(MIMEType, "video/"_ns);
+
+  if (!canForce &&
+      StaticPrefs::browser_download_sanitize_non_media_extensions()) {
+    for (const char* mime : forcedExtensionMimetypes) {
+      if (MIMEType.Equals(mime)) {
+        canForce = true;
+        break;
+      }
+    }
+  }
+  if (!canForce) {
+    return false;
+  }
+
+  // If we get here, we know for sure the mimetype allows us to overwrite the
+  // existing extension, if it's wrong. Return whether the extension is wrong:
+
+  bool knownExtension = false;
+  // Note that aFileExt is either empty or consists of an extension
+  // *including the dot* which we remove for ExtensionExists().
+  return (
+      aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") ||
+      (NS_SUCCEEDED(mMimeInfo->ExtensionExists(
+           Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) &&
+       !knownExtension));
+}
+
+void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) {
+  // If we don't have an extension (which will include the .),
+  // just short-circuit.
+  if (mTempFileExtension.Length() <= 1) {
+    return;
+  }
+
+  // After removing trailing whitespaces from the name, if we have a
+  // temp file extension, there are broadly 2 cases where we want to
+  // replace the extension.
+  // First, if the file extension contains invalid characters.
+  // Second, for document type mimetypes, if the extension is either
+  // missing or not valid for this mimetype.
+  bool replaceExtension =
+      (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) !=
+       kNotFound) ||
+      ShouldForceExtension(aFileExt);
+
+  if (replaceExtension) {
+    int32_t pos = mSuggestedFileName.RFindChar('.');
+    if (pos != kNotFound) {
+      mSuggestedFileName =
+          Substring(mSuggestedFileName, 0, pos) + mTempFileExtension;
+    } else {
+      mSuggestedFileName.Append(mTempFileExtension);
+    }
+  }
+
+  /*
+   * Ensure we don't double-append the file extension if it matches:
+   */
+  if (replaceExtension ||
+      aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) {
+    // Matches -> mTempFileExtension can be empty
+    mTempFileExtension.Truncate();
+  }
+}
+
 void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
   MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
   // Remove our request from the child loadGroup
   RetargetLoadNotifications(request);
 }
 
 NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
     nsIWebProgressListener2* aWebProgressListener) {
@@ -2521,25 +2788,25 @@ NS_IMETHODIMP nsExternalAppHandler::Prom
   if (mCanceled) return NS_OK;
 
   if (!StaticPrefs::browser_download_improvements_to_download_panel() ||
       mForceSave) {
     mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
   }
 
   if (mSuggestedFileName.IsEmpty()) {
-    RequestSaveDestination(mTempLeafName, mFileExtension);
+    RequestSaveDestination(mTempLeafName, mTempFileExtension);
   } else {
     nsAutoString fileExt;
     int32_t pos = mSuggestedFileName.RFindChar('.');
     if (pos >= 0) {
       mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
     }
     if (fileExt.IsEmpty()) {
-      fileExt = mFileExtension;
+      fileExt = mTempFileExtension;
     }
 
     RequestSaveDestination(mSuggestedFileName, fileExt);
   }
 
   return NS_OK;
 }
 nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
@@ -2657,24 +2924,17 @@ NS_IMETHODIMP nsExternalAppHandler::SetD
 
     if (mSuggestedFileName.IsEmpty()) {
       // Keep using the leafname of the temp file, since we're just starting a
       // helper
       mSuggestedFileName = mTempLeafName;
     }
 
 #ifdef XP_WIN
-    // Ensure we don't double-append the file extension if it matches:
-    if (StringEndsWith(mSuggestedFileName, mFileExtension,
-                       nsCaseInsensitiveStringComparator)) {
-      fileToUse->Append(mSuggestedFileName);
-    } else {
-      GetFileNameFromChannel fileToUse->Append(mSuggestedFileName +
-                                               mFileExtension);
-    }
+    fileToUse->Append(mSuggestedFileName + mTempFileExtension);
 #else
     fileToUse->Append(mSuggestedFileName);
 #endif
   }
 
   nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
   if (NS_SUCCEEDED(rv)) {
     mFinalFileDestination = fileToUse;
@@ -3219,479 +3479,8 @@ bool nsExternalHelperAppService::GetMIME
 
 nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
     const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
     nsIMIMEInfo** aMIMEInfo) {
   *aMIMEInfo = nullptr;
   *aFound = false;
   return NS_ERROR_NOT_IMPLEMENTED;
 }
-
-bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel,
-                                                        nsAString& aFileName,
-                                                        nsIURI** aURI) {
-  if (!aChannel) {
-    return false;
-  }
-
-  aChannel->GetURI(aURI);
-  nsCOMPtr<nsIURL> url = do_QueryInterface(*aURI);
-
-  // Check if we have a POST request, in which case we don't want to use
-  // the url's extension
-  bool allowURLExt = !net::ChannelIsPost(aChannel);
-
-  // Check if we had a query string - we don't want to check the URL
-  // extension if a query is present in the URI
-  // If we already know we don't want to check the URL extension, don't
-  // bother checking the query
-  if (url && allowURLExt) {
-    nsAutoCString query;
-
-    // We only care about the query for HTTP and HTTPS URLs
-    if (url->SchemeIs("http") || url->SchemeIs("https")) {
-      url->GetQuery(query);
-    }
-
-    // Only get the extension if the query is empty; if it isn't, then the
-    // extension likely belongs to a cgi script and isn't helpful
-    allowURLExt = query.IsEmpty();
-  }
-
-  aChannel->GetContentDispositionFilename(aFileName);
-
-  return allowURLExt;
-}
-
-NS_IMETHODIMP
-nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel,
-                                             const nsACString& aType,
-                                             nsIURI* aOriginalURI,
-                                             uint32_t aFlags,
-                                             nsAString& aOutFileName) {
-  nsCOMPtr<nsIURI> uri;
-  bool allowURLExtension =
-      GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri));
-
-  nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
-      aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsExternalHelperAppService::ValidateFileNameForSaving(
-    const nsAString& aFileName, const nsACString& aType, uint32_t aFlags,
-    nsAString& aOutFileName) {
-  nsAutoString fileName(aFileName);
-
-  // Just sanitize the filename only.
-  if (aFlags & VALIDATE_SANITIZE_ONLY) {
-    nsAutoString extension;
-    int32_t dotidx = fileName.RFind(".");
-    if (dotidx != -1) {
-      extension = Substring(fileName, dotidx + 1);
-    }
-
-    SanitizeFileName(fileName, NS_ConvertUTF16toUTF8(extension), aFlags);
-  } else {
-    nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
-        fileName, aType, nullptr, nullptr, aFlags, true);
-  }
-
-  aOutFileName = fileName;
-  return NS_OK;
-}
-
-already_AddRefed<nsIMIMEInfo>
-nsExternalHelperAppService::ValidateFileNameForSaving(
-    nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
-    nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) {
-  nsAutoString fileName(aFileName);
-  nsAutoCString extension;
-  nsCOMPtr<nsIMIMEInfo> mimeInfo;
-
-  bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) ||
-                      aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) ||
-                      aMimeType.EqualsLiteral("application/x-msdownload");
-
-  // We get the mime service here even though we're the default implementation
-  // of it, so it's possible to override only the mime service and not need to
-  // reimplement the whole external helper app service itself.
-  nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
-  if (mimeService) {
-    if (fileName.IsEmpty()) {
-      nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
-      // Try to extract the file name from the url and use that as a first
-      // pass as the leaf name of our temp file...
-      if (url) {
-        nsAutoCString leafName;
-        url->GetFileName(leafName);
-        if (!leafName.IsEmpty()) {
-          if (NS_SUCCEEDED(UnescapeFragment(leafName, url, fileName))) {
-            CopyUTF8toUTF16(leafName, aFileName);  // use escaped name
-          }
-        }
-
-        // Only get the extension from the URL if allowed, or if this
-        // is a binary type in which case the type might not be valid
-        // anyway.
-        if (aAllowURLExtension || isBinaryType) {
-          url->GetFileExtension(extension);
-        }
-      }
-    } else {
-      // Determine the current extension for the filename.
-      int32_t dotidx = fileName.RFind(".");
-      if (dotidx != -1) {
-        CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
-      }
-    }
-
-    if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) {
-      nsAutoCString mimeType;
-      if (!extension.IsEmpty()) {
-        mimeService->GetFromTypeAndExtension(EmptyCString(), extension,
-                                             getter_AddRefs(mimeInfo));
-        if (mimeInfo) {
-          mimeInfo->GetMIMEType(mimeType);
-        }
-      }
-
-      if (mimeType.IsEmpty()) {
-        // Extension lookup gave us no useful match, so use octet-stream
-        // instead.
-        mimeService->GetFromTypeAndExtension(
-            nsLiteralCString(APPLICATION_OCTET_STREAM), extension,
-            getter_AddRefs(mimeInfo));
-      }
-    } else if (!aMimeType.IsEmpty()) {
-      // If this is a binary type, include the extension as a hint to get
-      // the mime info. For other types, the mime type itself should be
-      // sufficient.
-      // The special case for application/ogg is because that type could
-      // actually be used for a video which can better be determined by the
-      // extension. This is tested by browser_save_video.js.
-      bool useExtension =
-          isBinaryType || aMimeType.EqualsLiteral(APPLICATION_OGG);
-      mimeService->GetFromTypeAndExtension(
-          aMimeType, useExtension ? extension : EmptyCString(),
-          getter_AddRefs(mimeInfo));
-      if (mimeInfo) {
-        // But if no primary extension was returned, this mime type is probably
-        // an unknown type. Look it up again but this time supply the extension.
-        nsAutoCString primaryExtension;
-        mimeInfo->GetPrimaryExtension(primaryExtension);
-        if (primaryExtension.IsEmpty()) {
-          mimeService->GetFromTypeAndExtension(aMimeType, extension,
-                                               getter_AddRefs(mimeInfo));
-        }
-      }
-    }
-  }
-
-  // Windows ignores terminating dots. So we have to as well, so
-  // that our security checks do "the right thing"
-  fileName.Trim(".", false);
-
-  // If an empty filename is allowed, then return early. It will be saved
-  // using the filename of the temporary file that was created for the download.
-  if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) {
-    aFileName.Truncate();
-    return mimeInfo.forget();
-  }
-
-  if (mimeInfo) {
-    bool isValidExtension;
-    if (extension.IsEmpty() ||
-        NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) ||
-        !isValidExtension) {
-      // Skip these checks for text and binary, so we don't append the unneeded
-      // .txt or other extension.
-      if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) {
-        extension.Truncate();
-      } else {
-        nsAutoCString originalExtension(extension);
-        // If an original url was supplied, see if it has a valid extension.
-        bool useOldExtension = false;
-        if (aOriginalURI) {
-          nsCOMPtr<nsIURL> originalURL(do_QueryInterface(aOriginalURI));
-          if (originalURL) {
-            originalURL->GetFileExtension(extension);
-            if (!extension.IsEmpty()) {
-              mimeInfo->ExtensionExists(extension, &useOldExtension);
-            }
-          }
-        }
-
-        if (!useOldExtension) {
-          // If the filename doesn't have a valid extension, or we don't know
-          // the extension, try to use the primary extension for the type. If we
-          // don't know the primary extension for the type, just continue with
-          // the existing extension, or leave the filename with no extension.
-          mimeInfo->GetPrimaryExtension(extension);
-        }
-
-        ModifyExtensionType modify =
-            ShouldModifyExtension(mimeInfo, originalExtension);
-        if (modify == ModifyExtension_Replace) {
-          int32_t dotidx = fileName.RFind(".");
-          if (dotidx != -1) {
-            // Remove the existing extension and replace it.
-            fileName.Truncate(dotidx);
-          }
-        }
-
-        // Otherwise, just append the proper extension to the end of the
-        // filename, adding to the invalid extension that might already be
-        // there.
-        if (modify != ModifyExtension_Ignore && !extension.IsEmpty()) {
-          fileName.AppendLiteral(".");
-          fileName.Append(NS_ConvertUTF8toUTF16(extension));
-        }
-      }
-    }
-  }
-
-#ifdef XP_WIN
-  nsLocalFile::CheckForReservedFileName(fileName);
-#endif
-
-  // If no filename is present, use a default filename.
-  if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) &&
-      (fileName.Length() == 0 || fileName.RFind(".") == 0)) {
-    nsCOMPtr<nsIStringBundleService> stringService =
-        mozilla::components::StringBundle::Service();
-    if (stringService) {
-      nsCOMPtr<nsIStringBundle> bundle;
-      if (NS_SUCCEEDED(stringService->CreateBundle(
-              "chrome://global/locale/contentAreaCommands.properties",
-              getter_AddRefs(bundle)))) {
-        nsAutoString defaultFileName;
-        bundle->GetStringFromName("DefaultSaveFileName", defaultFileName);
-        // Append any existing extension to the default filename.
-        fileName = defaultFileName + fileName;
-      }
-    }
-
-    // Use 'index' as a last resort.
-    if (!fileName.Length()) {
-      fileName.AssignLiteral("index");
-    }
-  }
-
-  // Make the filename safe for the filesystem
-  SanitizeFileName(fileName, extension, aFlags);
-
-  aFileName = fileName;
-  return mimeInfo.forget();
-}
-
-void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
-                                                  const nsACString& aExtension,
-                                                  uint32_t aFlags) {
-  nsAutoString fileName(aFileName);
-
-  // Replace characters
-  fileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
-  fileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
-  fileName.StripChar(char16_t(0));
-
-  const char16_t *startStr, *endStr;
-  fileName.BeginReading(startStr);
-  fileName.EndReading(endStr);
-
-  // True if multiple consecutive whitespace characters should
-  // be replaced by single space ' '.
-  bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
-
-  // The maximum filename length differs based on the platform:
-  //  Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units.
-  //  Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units.
-  //  Linux (ext3/ext4...) stores filenames with a maximum 255 bytes.
-  // So here we just use the maximum of 255 bytes.
-  uint32_t maxBytes = 0;  // 0 means don't truncate at a maximum size.
-  if (!(aFlags & VALIDATE_DONT_TRUNCATE)) {
-    maxBytes = 255 - aExtension.Length() - 1;
-  }
-
-  // True if the last character added was whitespace.
-  bool lastWasWhitespace = false;
-
-  // True if the filename is too long and must be truncated.
-  bool longFileName = false;
-
-  // Length of the filename that fits into the maximum size excluding the
-  // extension and period.
-  int32_t longFileNameEnd = -1;
-
-  // Index of the last character added that was not a character that can be
-  // trimmed off of the end of the string. Trimmable characters are whitespace,
-  // periods and the vowel separator u'\u180e'. If all the characters after this
-  // point are trimmable characters, truncate the string to this point after
-  // iterating over the filename.
-  int32_t lastNonTrimmable = -1;
-
-  // The number of bytes that the string would occupy if encoded in UTF-8.
-  uint32_t bytesLength = 0;
-
-  // This algorithm iterates over each character in the string and appends it
-  // or a replacement character if needed to outFileName.
-  nsAutoString outFileName;
-  while (startStr < endStr) {
-    bool err = false;
-    char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err);
-    if (err) {
-      break;
-    }
-
-    if (nextChar == char16_t(0)) {
-      continue;
-    }
-
-    auto unicodeCategory = unicode::GetGeneralCategory(nextChar);
-    if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL ||
-        unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR ||
-        unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) {
-      // Skip over any control characters and separators.
-      continue;
-    }
-
-    if (maxBytes) {
-      // UTF16CharEnumerator already converts surrogate pairs, so we can use
-      // a simple computation of byte length here.
-      bytesLength += nextChar < 0x80      ? 1
-                     : nextChar < 0x800   ? 2
-                     : nextChar < 0x10000 ? 3
-                                          : 4;
-      if (bytesLength > maxBytes) {
-        if (longFileNameEnd == -1) {
-          longFileNameEnd = int32_t(outFileName.Length());
-        }
-        if (bytesLength > 255) {
-          longFileName = true;
-          break;
-        }
-      }
-    }
-
-    if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR ||
-        nextChar == u'\ufeff') {
-      // Trim out any whitespace characters at the beginning of the filename,
-      // and only add whitespace in the middle of the filename if the last
-      // character was not whitespace or if we are not collapsing whitespace.
-      if (!outFileName.IsEmpty() &&
-          (!lastWasWhitespace || !collapseWhitespace)) {
-        // Allow the ideographic space if it is present, otherwise replace with
-        // ' '.
-        if (nextChar != u'\u3000') {
-          nextChar = ' ';
-        }
-        lastWasWhitespace = true;
-      } else {
-        lastWasWhitespace = true;
-        continue;
-      }
-    } else {
-      lastWasWhitespace = false;
-      if (nextChar == '.' || nextChar == u'\u180e') {
-        // Don't add any periods or vowel separators at the beginning of the
-        // string. Note also that lastNonTrimmable is not adjusted in this
-        // case, because periods and vowel separators are included in the
-        // set of characters to trim at the end of the filename.
-        if (outFileName.IsEmpty()) {
-          continue;
-        }
-      } else {
-        if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
-          // Replace formatting characters with an underscore.
-          nextChar = '_';
-        }
-
-        lastNonTrimmable = int32_t(outFileName.Length()) + 1;
-      }
-    }
-
-    AppendUCS4ToUTF16(nextChar, outFileName);
-  }
-
-  // There are two ways in which the filename should be truncated:
-  //   - If the filename was too long, truncate the name at the length
-  //     of the filename minus the space needed for the extension and period.
-  //     This position is indicated by longFileNameEnd.
-  //   - lastNonTrimmable will indicate the last character that was not
-  //     whitespace, a period, or a vowel separator at the end of the
-  //     the string, so the string should be truncated there as well.
-  // If both apply, use the earliest position.
-  if (lastNonTrimmable >= 0) {
-    outFileName.Truncate(longFileName
-                             ? std::min(longFileNameEnd, lastNonTrimmable)
-                             : lastNonTrimmable);
-  }
-
-  // If the filename is too long, truncate it, but preserve the desired
-  // extension.
-  if (!maxBytes && !(aFlags & VALIDATE_DONT_TRUNCATE) &&
-      outFileName.Length() > kDefaultMaxFileNameLength) {
-    // This is extremely unlikely, but if the extension is larger than the
-    // maximum size, just get rid of it.
-    if (aExtension.Length() >= kDefaultMaxFileNameLength) {
-      outFileName.Truncate(kDefaultMaxFileNameLength - 1);
-    } else {
-      outFileName.Truncate(kDefaultMaxFileNameLength - aExtension.Length() - 1);
-      longFileName = true;
-    }
-  }
-
-  if (longFileName && !outFileName.IsEmpty()) {
-    if (outFileName.Last() != '.') {
-      outFileName.AppendLiteral(".");
-    }
-
-    outFileName.Append(NS_ConvertUTF8toUTF16(aExtension));
-  }
-
-  aFileName = outFileName;
-}
-
-nsExternalHelperAppService::ModifyExtensionType
-nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
-                                                  const nsCString& aFileExt) {
-  nsAutoCString MIMEType;
-  if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) {
-    return ModifyExtension_Append;
-  }
-
-  // Determine whether the extensions should be appended or replaced depending
-  // on the content type.
-  bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
-                  StringBeginsWith(MIMEType, "audio/"_ns) ||
-                  StringBeginsWith(MIMEType, "video/"_ns);
-
-  if (!canForce) {
-    for (const char* mime : forcedExtensionMimetypes) {
-      if (MIMEType.Equals(mime)) {
-        if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) {
-          return ModifyExtension_Ignore;
-        }
-        canForce = true;
-        break;
-      }
-    }
-
-    if (!canForce) {
-      return ModifyExtension_Append;
-    }
-  }
-
-  // If we get here, we know for sure the mimetype allows us to modify the
-  // existing extension, if it's wrong. Return whether we should replace it
-  // or append it.
-  bool knownExtension = false;
-  // Note that aFileExt is either empty or consists of an extension
-  // excluding the dot.
-  if (aFileExt.IsEmpty() ||
-      (NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) &&
-       !knownExtension)) {
-    return ModifyExtension_Replace;
-  }
-
-  return ModifyExtension_Append;
-}
--- a/uriloader/exthandler/nsExternalHelperAppService.h
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -194,42 +194,16 @@ class nsExternalHelperAppService : publi
   void ExpungeTemporaryFiles();
   /**
    * Functions related to the tempory file cleanup service provided by
    * nsExternalHelperAppService (for the temporary files added during
    * the private browsing mode)
    */
   void ExpungeTemporaryPrivateFiles();
 
-  bool GetFileNameFromChannel(nsIChannel* aChannel, nsAString& aFileName,
-                              nsIURI** aURI);
-
-  // Internal version of the method from nsIMIMEService.
-  already_AddRefed<nsIMIMEInfo> ValidateFileNameForSaving(
-      nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
-      nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension);
-
-  void SanitizeFileName(nsAString& aFileName, const nsACString& aExtension,
-                        uint32_t aFlags);
-
-  /**
-   * Helper routine that checks how we should modify an extension
-   * for this file.
-   */
-  enum ModifyExtensionType {
-    // Replace an invalid extension with the preferred one.
-    ModifyExtension_Replace = 0,
-    // Append the preferred extension after any existing one.
-    ModifyExtension_Append = 1,
-    // Don't modify the extension.
-    ModifyExtension_Ignore = 2
-  };
-  ModifyExtensionType ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
-                                            const nsCString& aFileExt);
-
   /**
    * Array for the files that should be deleted
    */
   nsCOMArray<nsIFile> mTemporaryFilesList;
   /**
    * Array for the files that should be deleted (for the temporary files
    * added during the private browsing mode)
    */
@@ -272,25 +246,25 @@ class nsExternalAppHandler final : publi
    * @param aFileExtension  The extension we need to append to our temp file,
    *                        INCLUDING the ".". e.g. .mp3
    * @param aContentContext dom Window context, as passed to DoContent.
    * @param aWindowContext  Top level window context used in dialog parenting,
    *                        as passed to DoContent. This parameter may be null,
    *                        in which case dialogs will be parented to
    *                        aContentContext.
    * @param mExtProtSvc     nsExternalHelperAppService on creation
-   * @param aSuggestedFileName The filename to use
+   * @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 nsAString& aFileExtension,
+  nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension,
                        mozilla::dom::BrowsingContext* aBrowsingContext,
                        nsIInterfaceRequestor* aWindowContext,
                        nsExternalHelperAppService* aExtProtSvc,
-                       const nsAString& aSuggestedFileName, uint32_t aReason,
+                       const nsAString& aFilename, uint32_t aReason,
                        bool aForceSave);
 
   /**
    * Clean up after the request was diverted to the parent process.
    */
   void DidDivertRequest(nsIRequest* request);
 
   /**
@@ -305,17 +279,17 @@ class nsExternalAppHandler final : publi
   void RecordDownloadTelemetry(nsIChannel* aChannel, const char* aAction);
 
   bool IsDownloadSpam(nsIChannel* aChannel);
 
   ~nsExternalAppHandler();
 
   nsCOMPtr<nsIFile> mTempFile;
   nsCOMPtr<nsIURI> mSourceUrl;
-  nsString mFileExtension;
+  nsString mTempFileExtension;
   nsString mTempLeafName;
 
   /**
    * The MIME Info for this load. Will never be null.
    */
   nsCOMPtr<nsIMIMEInfo> mMimeInfo;
 
   /**
@@ -495,23 +469,29 @@ class nsExternalAppHandler final : publi
   void NotifyTransfer(nsresult aStatus);
 
   /**
    * Helper routine that searches a pref string for a given mime type
    */
   bool GetNeverAskFlagFromPref(const char* prefName, const char* aContentType);
 
   /**
+   * Helper routine that checks whether we should enforce an extension
+   * for this file.
+   */
+  bool ShouldForceExtension(const nsString& aFileExt);
+
+  /**
    * Helper routine to ensure that mSuggestedFileName ends in the correct
    * extension, in case the original extension contains invalid characters
    * or if this download is for a mimetype where we enforce using a specific
    * extension (image/, video/, and audio/ based mimetypes, and a few specific
    * document types).
    *
-   * It also ensure that mFileExtension only contains an extension
+   * It also ensure that mTempFileExtension only contains an extension
    * when it is different from mSuggestedFileName's extension.
    */
   void EnsureCorrectExtension(const nsString& aFileExt);
 
   typedef enum { kReadError, kWriteError, kLaunchError } ErrorType;
   /**
    * Utility function to send proper error notification to web progress listener
    */
--- a/uriloader/exthandler/tests/mochitest/browser.ini
+++ b/uriloader/exthandler/tests/mochitest/browser.ini
@@ -103,19 +103,16 @@ support-files =
 support-files =
   redirect_helper.sjs
 [browser_protocol_ask_dialog_permission.js]
 support-files =
   redirect_helper.sjs
   script_redirect.html
 [browser_protocolhandler_loop.js]
 [browser_remember_download_option.js]
-[browser_save_filenames.js]
-support-files =
-  save_filenames.html
 [browser_txt_download_save_as.js]
 support-files =
   file_txt_attachment_test.txt
   file_txt_attachment_test.txt^headers^
   !/toolkit/content/tests/browser/common/mockTransfer.js
 [browser_web_protocol_handlers.js]
 [browser_ftp_protocol_handlers.js]
 support-files =
--- a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
+++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
@@ -62,30 +62,18 @@ add_setup(async function() {
   const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
   const mimeInfo = MIMEService.getFromTypeAndExtension(
     "application/octet-stream",
     "bin"
   );
   mimeInfo.alwaysAskBeforeHandling = true;
   HandlerService.store(mimeInfo);
 
-  // On Mac, .bin is application/macbinary
-  let mimeInfoMac;
-  if (AppConstants.platform == "macosx") {
-    mimeInfoMac = MIMEService.getFromTypeAndExtension(
-      "application/macbinary",
-      "bin"
-    );
-    mimeInfoMac.alwaysAskBeforeHandling = true;
-    HandlerService.store(mimeInfoMac);
-  }
-
   registerCleanupFunction(() => {
     HandlerService.remove(mimeInfo);
-    HandlerService.remove(mimeInfoMac);
   });
 });
 
 add_task(async function simple_navigation() {
   // Tests that simple navigation gives us the right windowContext (that is,
   // the window that we're using).
   await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
     browser
deleted file mode 100644
--- a/uriloader/exthandler/tests/mochitest/browser_save_filenames.js
+++ /dev/null
@@ -1,737 +0,0 @@
-// There are at least seven different ways in a which a file can be saved or downloaded. This
-// test ensures that the filename is determined correctly when saving in these ways. The seven
-// ways are:
-//   - save the file individually from the File menu
-//   - save as complete web page (this uses a different codepath than the previous one)
-//   - dragging an image to the local file system
-//   - copy an image and paste it as a file to the local file system (windows only)
-//   - open a link with content-disposition set to attachment
-//   - open a link with the download attribute
-//   - save a link or image from the context menu
-
-requestLongerTimeout(5);
-
-let types = {
-  text: "text/plain",
-  png: "image/png",
-  jpeg: "image/jpeg",
-  webp: "image/webp",
-  otherimage: "image/unknown",
-  // Other js types (application/javascript and text/javascript) are handled by the system
-  // inconsistently, but application/x-javascript is handled by the external helper app service,
-  // so it is used here for this test.
-  js: "application/x-javascript",
-  binary: "application/octet-stream",
-  gook: "application/x-gook",
-};
-
-const PNG_DATA = atob(
-  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
-    "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
-);
-
-const JPEG_DATA = atob(
-  "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4z" +
-    "NDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEB" +
-    "AxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS" +
-    "0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz" +
-    "tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgEC" +
-    "BAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpj" +
-    "ZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6" +
-    "/9oADAMBAAIRAxEAPwD3+iiigD//2Q=="
-);
-
-const WEBP_DATA = atob(
-  "UklGRiIAAABXRUJQVlA4TBUAAAAvY8AYAAfQ/4j+B4CE8H+/ENH/VCIA"
-);
-
-const DEFAULT_INDEX_FILENAME =
-  AppConstants.platform == "win" ? "index.htm" : "index.html";
-
-const PROMISE_FILENAME_TYPE = "application/x-moz-file-promise-dest-filename";
-
-let MockFilePicker = SpecialPowers.MockFilePicker;
-MockFilePicker.init(window);
-
-let expectedItems;
-let sendAsAttachment = false;
-let httpServer = null;
-
-function handleRequest(aRequest, aResponse) {
-  const queryString = new URLSearchParams(aRequest.queryString);
-  let type = queryString.get("type");
-  let filename = queryString.get("filename");
-  let dispname = queryString.get("dispname");
-
-  aResponse.setStatusLine(aRequest.httpVersion, 200);
-  if (type) {
-    aResponse.setHeader("Content-Type", types[type]);
-  }
-
-  if (dispname) {
-    let dispositionType = sendAsAttachment ? "attachment" : "inline";
-    aResponse.setHeader(
-      "Content-Disposition",
-      dispositionType + ';name="' + dispname + '"'
-    );
-  } else if (filename) {
-    let dispositionType = sendAsAttachment ? "attachment" : "inline";
-    aResponse.setHeader(
-      "Content-Disposition",
-      dispositionType + ';filename="' + filename + '"'
-    );
-  } else if (sendAsAttachment) {
-    aResponse.setHeader("Content-Disposition", "attachment");
-  }
-
-  if (type == "png") {
-    aResponse.write(PNG_DATA);
-  } else if (type == "jpeg") {
-    aResponse.write(JPEG_DATA);
-  } else if (type == "webp") {
-    aResponse.write(WEBP_DATA);
-  } else {
-    aResponse.write("// Some Text");
-  }
-}
-
-function handleBasicImageRequest(aRequest, aResponse) {
-  aResponse.setHeader("Content-Type", "image/png");
-  aResponse.write(PNG_DATA);
-}
-
-function handleRedirect(aRequest, aResponse) {
-  const queryString = new URLSearchParams(aRequest.queryString);
-  let filename = queryString.get("filename");
-
-  aResponse.setStatusLine(aRequest.httpVersion, 302);
-  aResponse.setHeader("Location", "/bell" + filename[0] + "?" + queryString);
-}
-
-function promiseDownloadFinished(list) {
-  return new Promise(resolve => {
-    list.addView({
-      onDownloadChanged(download) {
-        if (download.stopped) {
-          list.removeView(this);
-          resolve(download);
-        }
-      },
-    });
-  });
-}
-
-// nsIFile::CreateUnique crops long filenames if the path is too long, but
-// we don't know exactly how long depending on the full path length, so
-// for those save methods that use CreateUnique, instead just verify that
-// the filename starts with the right string and has the correct extension.
-function checkShortenedFilename(actual, expected) {
-  if (actual.length < expected.length) {
-    let actualDot = actual.lastIndexOf(".");
-    let actualExtension = actual.substring(actualDot);
-    let expectedExtension = expected.substring(expected.lastIndexOf("."));
-    if (
-      actualExtension == expectedExtension &&
-      expected.startsWith(actual.substring(0, actualDot))
-    ) {
-      return true;
-    }
-  }
-
-  return false;
-}
-
-add_task(async function init() {
-  const { HttpServer } = ChromeUtils.import(
-    "resource://testing-common/httpd.js"
-  );
-  httpServer = new HttpServer();
-  httpServer.start(8000);
-
-  // Need to load the page from localhost:8000 as the download attribute
-  // only applies to links from the same domain.
-  let saveFilenamesPage = FileUtils.getFile(
-    "CurWorkD",
-    "/browser/uriloader/exthandler/tests/mochitest/save_filenames.html".split(
-      "/"
-    )
-  );
-  httpServer.registerFile("/save_filenames.html", saveFilenamesPage);
-
-  // A variety of different scripts are set up to better ensure uniqueness.
-  httpServer.registerPathHandler("/save_filename.sjs", handleRequest);
-  httpServer.registerPathHandler("/save_thename.sjs", handleRequest);
-  httpServer.registerPathHandler("/getdata.png", handleRequest);
-  httpServer.registerPathHandler("/base", handleRequest);
-  httpServer.registerPathHandler("/basedata", handleRequest);
-  httpServer.registerPathHandler("/basetext", handleRequest);
-  httpServer.registerPathHandler("/text2.txt", handleRequest);
-  httpServer.registerPathHandler("/text3.gonk", handleRequest);
-  httpServer.registerPathHandler("/basic.png", handleBasicImageRequest);
-  httpServer.registerPathHandler("/aquamarine.jpeg", handleBasicImageRequest);
-  httpServer.registerPathHandler("/lazuli.exe", handleBasicImageRequest);
-  httpServer.registerPathHandler("/redir", handleRedirect);
-  httpServer.registerPathHandler("/bellr", handleRequest);
-  httpServer.registerPathHandler("/bellg", handleRequest);
-  httpServer.registerPathHandler("/bellb", handleRequest);
-
-  await BrowserTestUtils.openNewForegroundTab(
-    gBrowser,
-    "http://localhost:8000/save_filenames.html"
-  );
-
-  expectedItems = await getItems("items");
-});
-
-function getItems(parentid) {
-  return SpecialPowers.spawn(
-    gBrowser.selectedBrowser,
-    [parentid, AppConstants.platform],
-    (id, platform) => {
-      let elements = [];
-      let elem = content.document.getElementById(id).firstElementChild;
-      while (elem) {
-        let filename =
-          elem.dataset["filenamePlatform" + platform] || elem.dataset.filename;
-        let url = elem.getAttribute("src");
-        let draggable =
-          elem.localName == "img" && elem.dataset.nodrag != "true";
-        let unknown = elem.dataset.unknown;
-        let noattach = elem.dataset.noattach;
-        elements.push({ draggable, unknown, filename, url, noattach });
-        elem = elem.nextElementSibling;
-      }
-      return elements;
-    }
-  );
-}
-
-function getDirectoryEntries(dir) {
-  let files = [];
-  let entries = dir.directoryEntries;
-  while (true) {
-    let file = entries.nextFile;
-    if (!file) {
-      break;
-    }
-    files.push(file.leafName);
-  }
-  entries.close();
-  return files;
-}
-
-// This test saves the document as a complete web page and verifies
-// that the resources are saved with the correct filename.
-add_task(async function save_document() {
-  let browser = gBrowser.selectedBrowser;
-
-  let tmp = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile);
-  const baseFilename = "test_save_filenames_" + Date.now();
-
-  let tmpFile = tmp.clone();
-  tmpFile.append(baseFilename + "_document.html");
-  let tmpDir = tmp.clone();
-  tmpDir.append(baseFilename + "_document_files");
-
-  MockFilePicker.displayDirectory = tmpDir;
-  MockFilePicker.showCallback = function(fp) {
-    MockFilePicker.setFiles([tmpFile]);
-    MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
-  };
-
-  let downloadsList = await Downloads.getList(Downloads.PUBLIC);
-  let savePromise = new Promise((resolve, reject) => {
-    downloadsList.addView({
-      onDownloadChanged(download) {
-        if (download.succeeded) {
-          downloadsList.removeView(this);
-          downloadsList.removeFinished();
-          resolve();
-        }
-      },
-    });
-  });
-  saveBrowser(browser);
-  await savePromise;
-
-  let filesSaved = getDirectoryEntries(tmpDir);
-
-  for (let idx = 0; idx < expectedItems.length; idx++) {
-    let filename = expectedItems[idx].filename;
-    if (idx == 66 && AppConstants.platform == "win") {
-      // This is special-cased on Windows. The default filename will be used, since
-      // the filename is invalid, but since the previous test file has the same issue,
-      // this second file will be saved with a number suffix added to it. -->
-      filename = "index_002";
-    }
-
-    let file = tmpDir.clone();
-    file.append(filename);
-
-    let fileIdx = -1;
-    // Use checkShortenedFilename to check long filenames.
-    if (filename.length > 240) {
-      for (let t = 0; t < filesSaved.length; t++) {
-        if (
-          filesSaved[t].length > 60 &&
-          checkShortenedFilename(filesSaved[t], filename)
-        ) {
-          fileIdx = t;
-          break;
-        }
-      }
-    } else {
-      fileIdx = filesSaved.indexOf(filename);
-    }
-
-    ok(
-      fileIdx >= 0,
-      "file i" +
-        idx +
-        " " +
-        filename +
-        " was saved with the correct name using saveDocument"
-    );
-    if (fileIdx >= 0) {
-      // If found, remove it from the list. At end of the test, the
-      // list should be empty.
-      filesSaved.splice(fileIdx, 1);
-    }
-  }
-
-  is(filesSaved.length, 0, "all files accounted for");
-  tmpDir.remove(true);
-  tmpFile.remove(false);
-});
-
-// This test simulates dragging the images in the document and ensuring that
-// the correct filename is used for each one.
-// On Mac, the data is added in the parent process instead, so we cannot
-// test dragging directly.
-if (AppConstants.platform != "macosx") {
-  add_task(async function drag_files() {
-    let browser = gBrowser.selectedBrowser;
-
-    await SpecialPowers.spawn(browser, [PROMISE_FILENAME_TYPE], type => {
-      content.addEventListener("dragstart", event => {
-        content.draggedFile = event.dataTransfer.getData(type);
-        event.preventDefault();
-      });
-    });
-
-    for (let idx = 0; idx < expectedItems.length; idx++) {
-      if (!expectedItems[idx].draggable) {
-        // You can't drag non-images and invalid images.
-        continue;
-      }
-
-      await BrowserTestUtils.synthesizeMouse(
-        "#i" + idx,
-        1,
-        1,
-        { type: "mousedown" },
-        browser
-      );
-      await BrowserTestUtils.synthesizeMouse(
-        "#i" + idx,
-        11,
-        11,
-        { type: "mousemove" },
-        browser
-      );
-      await BrowserTestUtils.synthesizeMouse(
-        "#i" + idx,
-        20,
-        20,
-        { type: "mousemove" },
-        browser
-      );
-      await BrowserTestUtils.synthesizeMouse(
-        "#i" + idx,
-        20,
-        20,
-        { type: "mouseup" },
-        browser
-      );
-
-      let draggedFile = await SpecialPowers.spawn(browser, [], () => {
-        let file = content.draggedFile;
-        content.draggedFile = null;
-        return file;
-      });
-
-      is(
-        draggedFile,
-        expectedItems[idx].filename,
-        "i" +
-          idx +
-          " " +
-          expectedItems[idx].filename +
-          " was saved with the correct name when dragging"
-      );
-    }
-  });
-}
-
-// This test checks that copying an image provides the right filename
-// for pasting to the local file system. This is only implemented on Windows.
-if (AppConstants.platform == "win") {
-  add_task(async function copy_image() {
-    for (let idx = 0; idx < expectedItems.length; idx++) {
-      if (!expectedItems[idx].draggable) {
-        // You can't context-click on non-images.
-        continue;
-      }
-
-      let data = await SpecialPowers.spawn(
-        gBrowser.selectedBrowser,
-        [idx, PROMISE_FILENAME_TYPE],
-        (imagenum, type) => {
-          // No need to wait for the data to be really on the clipboard, we only
-          // need the promise data added when the command is performed.
-          SpecialPowers.setCommandNode(
-            content,
-            content.document.getElementById("i" + imagenum)
-          );
-          SpecialPowers.doCommand(content, "cmd_copyImageContents");
-
-          return SpecialPowers.getClipboardData(type);
-        }
-      );
-
-      is(
-        data,
-        expectedItems[idx].filename,
-        "i" +
-          idx +
-          " " +
-          expectedItems[idx].filename +
-          " was saved with the correct name when copying"
-      );
-    }
-  });
-}
-
-// This test checks the default filename selected when selecting to save
-// a file from either the context menu or what would happen when save page
-// as was selected from the file menu. Note that this tests a filename assigned
-// when using content-disposition: inline.
-add_task(async function saveas_files() {
-  // Iterate over each item and try saving them from the context menu,
-  // and the Save Page As command on the File menu.
-  for (let testname of ["context menu", "save page as"]) {
-    for (let idx = 0; idx < expectedItems.length; idx++) {
-      let menu;
-      if (testname == "context menu") {
-        if (!expectedItems[idx].draggable) {
-          // You can't context-click on non-images.
-          continue;
-        }
-
-        menu = document.getElementById("contentAreaContextMenu");
-        let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
-        BrowserTestUtils.synthesizeMouse(
-          "#i" + idx,
-          5,
-          5,
-          { type: "contextmenu", button: 2 },
-          gBrowser.selectedBrowser
-        );
-        await popupShown;
-      } else {
-        if (expectedItems[idx].unknown == "typeonly") {
-          // Items marked with unknown="typeonly" have unknown content types and
-          // will be downloaded instead of opened in a tab.
-          let list = await Downloads.getList(Downloads.PUBLIC);
-          let downloadFinishedPromise = promiseDownloadFinished(list);
-
-          await BrowserTestUtils.openNewForegroundTab(
-            gBrowser,
-            expectedItems[idx].url
-          );
-
-          let download = await downloadFinishedPromise;
-
-          let filename = PathUtils.filename(download.target.path);
-          is(
-            filename,
-            expectedItems[idx].filename,
-            "open link" +
-              idx +
-              " " +
-              expectedItems[idx].filename +
-              " was downloaded with the correct name when opened as a url"
-          );
-
-          try {
-            await IOUtils.remove(download.target.path);
-          } catch (ex) {}
-
-          await BrowserTestUtils.removeTab(gBrowser.selectedTab);
-          continue;
-        }
-
-        await BrowserTestUtils.openNewForegroundTab(
-          gBrowser,
-          expectedItems[idx].url
-        );
-      }
-
-      let filename = await new Promise(resolve => {
-        MockFilePicker.showCallback = function(fp) {
-          setTimeout(() => {
-            resolve(fp.defaultString);
-          }, 0);
-          return Ci.nsIFilePicker.returnCancel;
-        };
-
-        if (testname == "context menu") {
-          let menuitem = document.getElementById("context-saveimage");
-          menu.activateItem(menuitem);
-        } else if (testname == "save page as") {
-          document.getElementById("Browser:SavePage").doCommand();
-        }
-      });
-
-      // Trying to open an unknown or binary type will just open a blank
-      // page, so trying to save will just save the blank page with the
-      // filename index.html.
-      let expectedFilename = expectedItems[idx].unknown
-        ? DEFAULT_INDEX_FILENAME
-        : expectedItems[idx].filename;
-
-      // When saving via contentAreaUtils.js, the content disposition name
-      // field is used as an alternate.
-      if (expectedFilename == "save_thename.png") {
-        expectedFilename = "withname.png";
-      }
-
-      is(
-        filename,
-        expectedFilename,
-        "i" +
-          idx +
-          " " +
-          expectedFilename +
-          " was saved with the correct name " +
-          testname
-      );
-
-      if (testname == "save page as") {
-        await BrowserTestUtils.removeTab(gBrowser.selectedTab);
-      }
-    }
-  }
-});
-
-// This test checks that links that result in files with
-// content-disposition: attachment are saved with the right filenames.
-add_task(async function save_links() {
-  sendAsAttachment = true;
-
-  // Create some links based on each image and insert them into the document.
-  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
-    let doc = content.document;
-    let insertPos = doc.getElementById("attachment-links");
-
-    let idx = 0;
-    let elem = doc.getElementById("items").firstElementChild;
-    while (elem) {
-      let attachmentlink = doc.createElement("a");
-      attachmentlink.id = "attachmentlink" + idx;
-      attachmentlink.href = elem.localName == "object" ? elem.data : elem.src;
-      attachmentlink.textContent = elem.dataset.filename;
-      insertPos.appendChild(attachmentlink);
-      insertPos.appendChild(doc.createTextNode("  "));
-
-      elem = elem.nextElementSibling;
-      idx++;
-    }
-  });
-
-  let list = await Downloads.getList(Downloads.PUBLIC);
-
-  for (let idx = 0; idx < expectedItems.length; idx++) {
-    // Skip the items that won't have a content-disposition.
-    if (expectedItems[idx].noattach) {
-      continue;
-    }
-
-    let downloadFinishedPromise = promiseDownloadFinished(list);
-
-    BrowserTestUtils.synthesizeMouse(
-      "#attachmentlink" + idx,
-      5,
-      5,
-      {},
-      gBrowser.selectedBrowser
-    );
-
-    let download = await downloadFinishedPromise;
-
-    let filename = PathUtils.filename(download.target.path);
-
-    let expectedFilename = expectedItems[idx].filename;
-    if (AppConstants.platform == "win" && idx == 54) {
-      // On Windows, .txt is added when saving as an attachment
-      // to avoid this looking like an executable. This
-      // is done in validateLeafName in HelperAppDlg.jsm.
-      // XXXndeakin should we do this for all save mechanisms?
-      expectedFilename += ".txt";
-    }
-
-    // Use checkShortenedFilename to check long filenames.
-    if (expectedItems[idx].filename.length > 240) {
-      ok(
-        checkShortenedFilename(filename, expectedFilename),
-        "attachmentlink" +
-          idx +
-          " " +
-          expectedFilename +
-          " was saved with the correct name when opened as attachment (with long name)"
-      );
-    } else {
-      is(
-        filename,
-        expectedFilename,
-        "attachmentlink" +
-          idx +
-          " " +
-          expectedFilename +
-          " was saved with the correct name when opened as attachment"
-      );
-    }
-
-    try {
-      await IOUtils.remove(download.target.path);
-    } catch (ex) {}
-  }
-
-  sendAsAttachment = false;
-});
-
-// This test checks some cases where links to images are saved using Save Link As,
-// and when opening them in a new tab and then using Save Page As.
-add_task(async function saveas_image_links() {
-  let links = await getItems("links");
-
-  // Iterate over each link and try saving the links from the context menu,
-  // and then after opening a new tab for that link and then selecting
-  // the Save Page As command on the File menu.
-  for (let testname of ["save link as", "save link then save page as"]) {
-    for (let idx = 0; idx < links.length; idx++) {
-      let menu = document.getElementById("contentAreaContextMenu");
-      let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
-      BrowserTestUtils.synthesizeMouse(
-        "#link" + idx,
-        5,
-        5,
-        { type: "contextmenu", button: 2 },
-        gBrowser.selectedBrowser
-      );
-      await popupShown;
-
-      let promptPromise = new Promise(resolve => {
-        MockFilePicker.showCallback = function(fp) {
-          setTimeout(() => {
-            resolve(fp.defaultString);
-          }, 0);
-          return Ci.nsIFilePicker.returnCancel;
-        };
-      });
-
-      if (testname == "save link as") {
-        let menuitem = document.getElementById("context-savelink");
-        menu.activateItem(menuitem);
-      } else {
-        let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-
-        let menuitem = document.getElementById("context-openlinkintab");
-        menu.activateItem(menuitem);
-
-        let tab = await newTabPromise;
-        await BrowserTestUtils.switchTab(gBrowser, tab);
-
-        document.getElementById("Browser:SavePage").doCommand();
-      }
-
-      let filename = await promptPromise;
-
-      let expectedFilename = links[idx].filename;
-      // Only codepaths that go through contentAreaUtils.js use the
-      // name from the content disposition.
-      if (testname == "save link as" && expectedFilename == "four.png") {
-        expectedFilename = "save_filename.png";
-      }
-
-      is(
-        filename,
-        expectedFilename,
-        "i" +
-          idx +
-          " " +
-          expectedFilename +
-          " link was saved with the correct name " +
-          testname
-      );
-
-      if (testname == "save link then save page as") {
-        await BrowserTestUtils.removeTab(gBrowser.selectedTab);
-      }
-    }
-  }
-});
-
-// This test checks that links that with a download attribute
-// are saved with the right filenames.
-add_task(async function save_download_links() {
-  let downloads = await getItems("downloads");
-
-  let list = await Downloads.getList(Downloads.PUBLIC);
-  for (let idx = 0; idx < downloads.length; idx++) {
-    let downloadFinishedPromise = promiseDownloadFinished(list);
-
-    BrowserTestUtils.synthesizeMouse(
-      "#download" + idx,
-      2,
-      2,
-      {},
-      gBrowser.selectedBrowser
-    );
-
-    let download = await downloadFinishedPromise;
-
-    let filename = PathUtils.filename(download.target.path);
-
-    if (downloads[idx].filename.length > 240) {
-      ok(
-        checkShortenedFilename(filename, downloads[idx].filename),
-        "download" +
-          idx +
-          " " +
-          downloads[idx].filename +
-          " was saved with the correct name when link has download attribute"
-      );
-    } else {
-      is(
-        filename,
-        downloads[idx].filename,
-        "download" +
-          idx +
-          " " +
-          downloads[idx].filename +
-          " was saved with the correct name when link has download attribute"
-      );
-    }
-
-    try {
-      await IOUtils.remove(download.target.path);
-    } catch (ex) {}
-  }
-});
-
-add_task(async () => {
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  MockFilePicker.cleanup();
-  await new Promise(resolve => httpServer.stop(resolve));
-});
deleted file mode 100644
--- a/uriloader/exthandler/tests/mochitest/save_filenames.html
+++ /dev/null
@@ -1,296 +0,0 @@
-<html>
-<head>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-</head>
-<body>
-<style>
-  img { padding: 10px; border: 1px solid red; }
-  a { padding-left: 10px; }
-</style>
-
-<span id="items">
-
-<!-- simple filename -->
-<img id="i0" src="http://localhost:8000/basic.png"
-     data-noattach="true" data-filename="basic.png">
-
-<!-- simple filename with content disposition -->
-<img id="i1" src="http://localhost:8000/save_filename.sjs?type=png&filename=simple.png" data-filename="simple.png">
-
-<!-- invalid characters in the filename -->
-<img id="i2" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidfilename/a:b*c%63d.png" data-filename="invalidfilename_a b ccd.png">
-
-<!-- invalid extension for a png image -->
-<img id="i3" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidextension.pang" data-filename="invalidextension.png">
-
-<!-- jpeg extension for a png image -->
-<img id="i4" src="http://localhost:8000/save_filename.sjs?type=png&filename=reallyapng.jpeg" data-filename="reallyapng.png">
-
-<!-- txt extension for a png image -->
-<img id="i5" src="http://localhost:8000/save_filename.sjs?type=png&filename=nottext.txt" data-filename="nottext.png">
-
-<!-- no extension for a png image -->
-<img id="i6" src="http://localhost:8000/save_filename.sjs?type=png&filename=noext" data-filename="noext.png">
-
-<!-- empty extension for a png image -->
-<img id="i7" src="http://localhost:8000/save_filename.sjs?type=png&filename=noextdot." data-filename="noextdot.png">
-
-<!-- long filename -->
-<img id="i8" src="http://localhost:8000/save_filename.sjs?type=png&filename=averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.png"
-     data-filename="averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png">
-
-<!-- long filename with invalid extension -->
-<img id="i9" src="http://localhost:8000/save_filename.sjs?type=png&filename=bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe"
-     data-filename="bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png">
-
-<!-- long filename with invalid extension -->
-<img id="i10" src="http://localhost:8000/save_filename.sjs?type=png&filename=cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe.jpg"
-     data-filename="cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png">
-
-<!-- jpeg with jpg extension -->
-<img id="i11" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpg" data-filename="thejpg.jpg">
-
-<!-- jpeg with jpeg extension -->
-<img id="i12" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpeg" data-filename="thejpg.jpeg">
-
-<!-- jpeg with invalid extension -->
-<img id="i13" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=morejpg.exe" data-filename="morejpg.jpg" data-filename-platformlinux="morejpg.jpeg">
-
-<!-- jpeg with multiple extensions -->
-<img id="i14" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=anotherjpg.jpg.exe" data-filename="anotherjpg.jpg.jpg" data-filename-platformlinux="anotherjpg.jpg.jpeg">
-
-<!-- jpeg with no filename portion -->
-<img id="i15" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=.jpg" data-filename="index.jpg">
-
-<!-- png with no filename portion and invalid extension -->
-<img id="i16" src="http://localhost:8000/save_filename.sjs?type=png&filename=.exe" data-filename="index.png">
-
-<!-- png with escaped characters -->
-<img id="i17" src="http://localhost:8000/save_filename.sjs?type=png&filename=first%20file.png" data-filename="first file.png">
-
-<!-- png with more escaped characters -->
-<img id="i18" src="http://localhost:8000/save_filename.sjs?type=png&filename=second%32file%2Eexe" data-filename="second2file.png">
-
-<!-- unknown type with png extension -->
-<img id="i19" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook1.png"
-     data-nodrag="true" data-unknown="typeonly" data-filename="gook1.png">
-
-<!-- unknown type with exe extension -->
-<img id="i20" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook2.exe"
-     data-nodrag="true" data-unknown="typeonly" data-filename="gook2.exe">
-
-<!-- unknown type with no extension -->
-<img id="i21" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook3"
-     data-nodrag="true" data-unknown="typeonly" data-filename="gook3">
-
-<!-- simple script -->
-<script id="i22" src="http://localhost:8000/save_filename.sjs?type=js&filename=script1.js" data-filename="script1.js"></script>
-
-<!-- script with invalid extension. Windows doesn't have an association for application/x-javascript
-     so doesn't handle it. -->
-<script id="i23" src="http://localhost:8000/save_filename.sjs?type=js&filename=script2.exe"
-        data-filename="script2.exe.js"></script>
-
-<!-- script with escaped characters -->
-<script id="i24" src="http://localhost:8000/save_filename.sjs?type=js&filename=script%20%33.exe"
-        data-filename="script 3.exe.js"></script>
-
-<!-- script with long filename -->
-<script id="i25" src="http://localhost:8000/save_filename.sjs?type=js&filename=script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789.js"
-        data-filename="script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456.js"></script>
-
-<!-- binary with exe extension -->
-<object id="i26" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download1.exe"
-        data-unknown="true" data-filename="download1.exe"></object>
-
-<!-- binary with invalid extension -->
-<object id="i27" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download2.png"
-        data-unknown="true" data-filename="download2.png"></object>
-
-<!-- binary with no extension -->
-<object id="i28" data="http://localhost:8000/save_filename.sjs?type=binary&filename=downloadnoext"
-        data-unknown="true" data-filename="downloadnoext"></object>
-
-<!-- binary with no other invalid characters -->
-<object id="i29" data="http://localhost:8000/save_filename.sjs?type=binary&filename=binary^%31%20exe.exe"
-        data-unknown="true" data-filename="binary^1 exe.exe"></object>
-
-<!-- unknown image type with no extension, but ending in png -->
-<img id="i30" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=specialpng"
-     data-unknown="typeonly" data-nodrag="true" data-filename="specialpng">
-
-<!-- unknown image type with no extension, but ending in many dots -->
-<img id="i31" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=extrapng..."
-     data-unknown="typeonly" data-nodrag="true" data-filename="extrapng">
-
-<!-- image type with no content-disposition filename specified -->
-<img id="i32" src="http://localhost:8000/save_filename.sjs?type=png" data-filename="save_filename.png">
-
-<!-- binary with no content-disposition filename specified -->
-<object id="i33" data="http://localhost:8000/save_filename.sjs?type=binary"
-        data-unknown="true" data-filename="save_filename.sjs"></object>
-
-<!-- image where url has png extension -->
-<img id="i34" src="http://localhost:8000/getdata.png?type=png&filename=override.png" data-filename="override.png">
-
-<!-- image where url has png extension but content disposition has incorrect extension -->
-<img id="i35" src="http://localhost:8000/getdata.png?type=png&filename=flower.jpeg" data-filename="flower.png">
-
-<!-- image where url has png extension but content disposition does not -->
-<img id="i36" src="http://localhost:8000/getdata.png?type=png&filename=ruby" data-filename="ruby.png">
-
-<!-- image where url has png extension but content disposition has invalid characters -->
-<img id="i37" src="http://localhost:8000/getdata.png?type=png&filename=sapphire/data" data-filename="sapphire_data.png">
-
-<!-- image where neither content disposition or url have an extension -->
-<img id="i38" src="http://localhost:8000/base?type=png&filename=emerald" data-filename="emerald.png">
-
-<!-- image where filename is not specified -->
-<img id="i39" src="http://localhost:8000/base?type=png" data-filename="base.png">
-
-<!-- simple script where url filename has no extension -->
-<script id="i40" src="http://localhost:8000/base?type=js&filename=script4.js" data-filename="script4.js"></script>
-
-<!-- script where url filename has no extension and invalid extension in content disposition filename -->
-<script id="i41" src="http://localhost:8000/base?type=js&filename=script5.exe"
-        data-filename="script5.exe.js"></script>
-
-<!-- script where url filename has no extension and escaped characters in content disposition filename-->
-<script id="i42" src="http://localhost:8000/base?type=js&filename=script%20%36.exe"
-        data-filename="script 6.exe.js"></script>
-
-<!-- text where filename is present -->
-<img id="i43" src="http://localhost:8000/getdata.png?type=text&filename=readme.txt"
-     data-nodrag="true" data-filename="readme.txt">
-
-<!-- text where filename is present with a different extension -->
-<img id="i44" src="http://localhost:8000/getdata.png?type=text&filename=main.cpp"
-     data-nodrag="true" data-filename="main.cpp">
-
-<!-- text where extension is not present -->
-<img id="i45" src="http://localhost:8000/getdata.png?type=text&filename=readme"
-     data-nodrag="true" data-filename="readme">
-
-<!-- text where extension is not present and url does not have extension -->
-<img id="i46" src="http://localhost:8000/base?type=text&filename=info"
-     data-nodrag="true" data-filename="info">
-
-<!-- text where filename is not present -->
-<img id="i47" src="http://localhost:8000/basetext?type=text"
-     data-nodrag="true" data-filename="basetext">
-
-<!-- text where url has extension -->
-<img id="i48" src="http://localhost:8000/text2.txt?type=text"
-     data-nodrag="true" data-filename="text2.txt">
-
-<!-- text where url has extension -->
-<img id="i49" src="http://localhost:8000/text3.gonk?type=text"
-     data-nodrag="true" data-filename="text3.gonk">
-
-<!-- text with long filename -->
-<img id="i50" src="http://localhost:8000/text3.gonk?type=text&filename=text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789text0123456789zztext0123456789zztext0123456789zztext01234567.exe.txt" data-nodrag="true" data-filename="text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456.txt">
-
-<!-- webp image -->
-<img id="i51" src="http://localhost:8000/save_filename.sjs?type=webp&filename=webpimage.webp"
-     data-filename="webpimage.webp">
-
-<!-- webp image with jpg extension -->
-<img id="i52" src="http://localhost:8000/save_filename.sjs?type=webp&filename=realwebpimage.jpg"
-     data-filename="realwebpimage.webp">
-
-<!-- no content type specified -->
-<img id="i53" src="http://localhost:8000/save_filename.sjs?&filename=notype.png"
-     data-nodrag="true" data-filename="notype.png">
-
-<!-- no content type specified. Note that on Windows, .txt is
-     appended when saving as an attachment. This is special-cased
-     in browser_save_filenames.js. -->
-<img id="i54" src="http://localhost:8000/save_filename.sjs?&filename=notypebin.exe"
-     data-nodrag="true" data-filename="notypebin.exe">
-
-<!-- extension contains invalid characters -->
-<img id="i55" src="http://localhost:8000/save_filename.sjs?type=png&filename=extinvalid.a?*"
-     data-filename="extinvalid.png">
-
-<!-- filename with redirect and content disposition -->
-<img id="i56" src="http://localhost:8000/redir?type=png&filename=red.png" data-filename="red.png">
-
-<!-- filename with redirect and different type -->
-<img id="i57" src="http://localhost:8000/redir?type=jpeg&filename=green.png"
-     data-filename="green.jpg" data-filename-platformlinux="green.jpeg">
-
-<!-- filename with redirect and binary type -->
-<object id="i58" data="http://localhost:8000/redir?type=binary&filename=blue.png"
-        data-unknown="true" data-filename="blue.png"></object>
-
-<!-- filename in url with incorrect extension -->
-<img id="i59" src="http://localhost:8000/aquamarine.jpeg"
-     data-noattach="true" data-filename="aquamarine.png">
-
-<!-- filename in url with exe extension, but returns a png image -->
-<img id="i60" src="http://localhost:8000/lazuli.exe"
-     data-noattach="true" data-filename="lazuli.png">
-
-<!-- filename with leading, trailing and duplicate spaces -->
-<img id="i61" src="http://localhost:8000/save_filename.sjs?type=png&filename=  with  spaces.png  "
-     data-filename="with spaces.png">
-
-<!-- filename with leading and trailing periods -->
-<img id="i62" src="http://localhost:8000/save_filename.sjs?type=png&filename=..with..dots..png.."
-     data-filename="with..dots..png">
-
-<!-- filename with non-ascii character -->
-<img id="i63" src="http://localhost:8000/base?type=png&filename=s%C3%B6meescapes.%C3%B7ng" data-filename="sömeescapes.png">
-
-<!-- filename with content disposition name assigned. The name is only used
-     when selecting to manually save, otherwise it is ignored.  -->
-<img id="i64" src="http://localhost:8000/save_thename.sjs?type=png&dispname=withname"
-     data-filename="save_thename.png">
-
-<!-- reserved filename on Windows -->
-<img id="i65" src="http://localhost:8000/save_filename.sjs?type=text&filename=com1"
-     data-nodrag="true" data-filename="com1" data-filename-platformwin="index">
-
-<!-- reserved filename with extension on Windows -->
-<img id="i66" src="http://localhost:8000/save_filename.sjs?type=text&filename=com2.any"
-     data-nodrag="true" data-filename="com2.any" data-filename-platformwin="index">
-
-</span>
-
-<!-- This set is used to test the filename specified by the download attribute is validated correctly. -->
-<span id="downloads">
-  <a id="download0" href="http://localhost:8000/base" download="pearl.png" data-filename="pearl.png">Link</a>
-  <a id="download1" href="http://localhost:8000/save_filename.sjs?type=png" download="opal.jpeg" data-filename="opal.png">Link</a>
-  <a id="download2" href="http://localhost:8000/save_filename.sjs?type=jpeg"
-     download="amethyst.png" data-filename="amethyst.jpg"
-     data-filename-platformlinux="amethyst.jpeg">Link</a>
-  <a id="download3" href="http://localhost:8000/save_filename.sjs?type=text"
-     download="onyx.png" data-filename="onyx.png">Link</a>
-  <!-- The content-disposition overrides the download attribute. -->
-  <a id="download4" href="http://localhost:8000/save_filename.sjs?type=png&filename=fakename.jpeg" download="topaz.jpeg" data-filename="fakename.png">Link</a>
-  <a id="download5" href="http://localhost:8000/save_filename.sjs?type=png"
-     download="amber?.png" data-filename="amber .png">Link</a>
-  <a id="download6" href="http://localhost:8000/save_filename.sjs?type=jpeg"
-     download="jade.:*jpeg" data-filename="jade.jpg"
-     data-filename-platformlinux="jade.jpeg">Link</a>>
-  <a id="download7" href="http://localhost:8000/save_filename.sjs?type=png"
-     download="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename.png"
-     data-filename="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisavery.png">Link</a>
-  <a id="download8" href="http://localhost:8000/base"
-      download="&Tab;&NewLine;&nbsp;&#11&#x180e;&#x180e;&#12   spa   ced.png &Tab;&NewLine;&nbsp;&#x180e;&#x180e;&#11&#12 "
-      data-filename="spa ced.png">Link</a>
-</span>
-
-<span id="links">
-  <a id="link0" href="http://localhost:8000/save_filename.sjs?type=png&filename=one.png" data-filename="one.png">One</a>
-  <a id="link1" href="http://localhost:8000/save_filename.sjs?type=png&filename=two.jpeg" data-filename="two.png">Two</a>
-  <a id="link2" href="http://localhost:8000/save_filename.sjs?type=png&filename=three.con" data-filename="three.png">Three</a>
-  <a id="link3" href="http://localhost:8000/save_filename.sjs?type=png&dispname=four" data-filename="four.png">Four</a>
-</span>
-
-<!-- The content-disposition attachment generates links from the images/objects/scripts above
-     and inserts them here. -->
-<p id="attachment-links">
-</p>
-
-</body></html>
deleted file mode 100644
--- a/uriloader/exthandler/tests/unit/test_filename_sanitize.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// This test verifies that
-// nsIMIMEService.validateFileNameForSaving sanitizes filenames
-// properly with different flags.
-
-"use strict";
-
-add_task(async function validate_filename_method() {
-  let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
-
-  function checkFilename(filename, flags) {
-    return mimeService.validateFileNameForSaving(filename, "image/png", flags);
-  }
-
-  Assert.equal(checkFilename("basicfile.png", 0), "basicfile.png");
-  Assert.equal(checkFilename("  whitespace.png  ", 0), "whitespace.png");
-  Assert.equal(
-    checkFilename(" .whitespaceanddots.png...", 0),
-    "whitespaceanddots.png"
-  );
-  Assert.equal(
-    checkFilename("  \u00a0 \u00a0 extrawhitespace.png  \u00a0 \u00a0 ", 0),
-    "extrawhitespace.png"
-  );
-  Assert.equal(
-    checkFilename("  filename  with  whitespace.png  ", 0),
-    "filename with whitespace.png"
-  );
-  Assert.equal(checkFilename("\\path.png", 0), "_path.png");
-  Assert.equal(
-    checkFilename("\\path*and/$?~file.png", 0),
-    "_path and_$ ~file.png"
-  );
-  Assert.equal(
-    checkFilename(" \u180e whit\u180ee.png \u180e", 0),
-    "whit\u180ee.png"
-  );
-  Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png");
-  Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png");
-  Assert.equal(
-    checkFilename("12345678".repeat(31) + "abcdefgh.png", 0),
-    "12345678".repeat(31) + "abc.png"
-  );
-  Assert.equal(
-    checkFilename("簡単".repeat(41) + ".png", 0),
-    "簡単".repeat(41) + ".png"
-  );
-  Assert.equal(
-    checkFilename("簡単".repeat(42) + ".png", 0),
-    "簡単".repeat(41) + "簡.png"
-  );
-  Assert.equal(
-    checkFilename("簡単".repeat(56) + ".png", 0),
-    "簡単".repeat(41) + "簡.png"
-  );
-  Assert.equal(checkFilename("café.png", 0), "café.png");
-  Assert.equal(
-    checkFilename("café".repeat(50) + ".png", 0),
-    "café".repeat(50) + ".png"
-  );
-  Assert.equal(
-    checkFilename("café".repeat(51) + ".png", 0),
-    "café".repeat(50) + "c.png"
-  );
-
-  Assert.equal(
-    checkFilename("\u{100001}\u{100002}.png", 0),
-    "\u{100001}\u{100002}.png"
-  );
-  Assert.equal(
-    checkFilename("\u{100001}\u{100002}".repeat(31) + ".png", 0),
-    "\u{100001}\u{100002}".repeat(31) + ".png"
-  );
-  Assert.equal(
-    checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0),
-    "\u{100001}\u{100002}".repeat(31) + ".png"
-  );
-
-  Assert.equal(
-    checkFilename("noextensionfile".repeat(16), 0),
-    "noextensionfile".repeat(16) + ".png"
-  );
-  Assert.equal(
-    checkFilename("noextensionfile".repeat(17), 0),
-    "noextensionfile".repeat(16) + "noextension.png"
-  );
-  Assert.equal(
-    checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0),
-    "noextensionfile".repeat(16) + "noextension.png"
-  );
-
-  Assert.equal(checkFilename("  first  .png  ", 0), "first .png");
-  Assert.equal(
-    checkFilename(
-      "  second  .png  ",
-      mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE
-    ),
-    "second  .png"
-  );
-
-  // For whatever reason, the Android mime handler accepts the .jpeg
-  // extension for image/png, so skip this test there.
-  if (AppConstants.platform != "android") {
-    Assert.equal(checkFilename("thi/*rd.jpeg", 0), "thi_ rd.png");
-  }
-
-  Assert.equal(
-    checkFilename("f*\\ourth  file.jpg", mimeService.VALIDATE_SANITIZE_ONLY),
-    "f _ourth file.jpg"
-  );
-  Assert.equal(
-    checkFilename(
-      "f*\\ift  h.jpe*\\g",
-      mimeService.VALIDATE_SANITIZE_ONLY |
-        mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE
-    ),
-    "f _ift  h.jpe _g"
-  );
-  Assert.equal(checkFilename("sixth.j  pe/*g", 0), "sixth.png");
-
-  let repeatStr = "12345678".repeat(31);
-  Assert.equal(
-    checkFilename(
-      repeatStr + "seventh.png",
-      mimeService.VALIDATE_DONT_TRUNCATE
-    ),
-    repeatStr + "seventh.png"
-  );
-  Assert.equal(
-    checkFilename(repeatStr + "seventh.png", 0),
-    repeatStr + "sev.png"
-  );
-});
--- a/uriloader/exthandler/tests/unit/xpcshell.ini
+++ b/uriloader/exthandler/tests/unit/xpcshell.ini
@@ -10,17 +10,16 @@ skip-if =
     os == "android"
     appname == "thunderbird"
 [test_downloads_improvements_migration.js]
 # No default stored handlers on android given lack of support.
 # No default stored handlers on Thunderbird.
 skip-if =
     os == "android"
     appname == "thunderbird"
-[test_filename_sanitize.js]
 [test_getFromTypeAndExtension.js]
 [test_getMIMEInfo_pdf.js]
 [test_getMIMEInfo_unknown_mime_type.js]
 run-if = os == "win" # Windows only test
 [test_getTypeFromExtension_ext_to_type_mapping.js]
 [test_getTypeFromExtension_with_empty_Content_Type.js]
 run-if = os == "win" # Windows only test
 [test_badMIMEType.js]
--- a/widget/windows/nsDataObj.cpp
+++ b/widget/windows/nsDataObj.cpp
@@ -39,17 +39,16 @@
 #include "mozilla/Preferences.h"
 #include "nsContentUtils.h"
 #include "nsIPrincipal.h"
 #include "nsNativeCharsetUtils.h"
 #include "nsMimeTypes.h"
 #include "imgIEncoder.h"
 #include "imgITools.h"
 #include "WinUtils.h"
-#include "nsLocalFile.h"
 
 #include "mozilla/LazyIdleThread.h"
 #include <algorithm>
 
 using namespace mozilla;
 using namespace mozilla::glue;
 using namespace mozilla::widget;
 
@@ -1084,28 +1083,58 @@ nsDataObj ::GetFileContents(FORMATETC& a
   else
     NS_WARNING("Not yet implemented\n");
 
   return res;
 
 }  // GetFileContents
 
 //
+// Given a unicode string, we ensure that it contains only characters which are
+// valid within the file system. Remove all forbidden characters from the name,
+// and completely disallow any title that starts with a forbidden name and
+// extension (e.g. "nul" is invalid, but "nul." and "nul.txt" are also invalid
+// and will cause problems).
+//
+// It would seem that this is more functionality suited to being in nsIFile.
+//
+static void MangleTextToValidFilename(nsString& aText) {
+  static const char* forbiddenNames[] = {
+      "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",  "COM8",
+      "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6",  "LPT7",
+      "LPT8", "LPT9", "CON",  "PRN",  "AUX",  "NUL",  "CLOCK$"};
+
+  aText.StripChars(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS);
+  aText.CompressWhitespace(true, true);
+  uint32_t nameLen;
+  for (size_t n = 0; n < ArrayLength(forbiddenNames); ++n) {
+    nameLen = (uint32_t)strlen(forbiddenNames[n]);
+    if (aText.EqualsIgnoreCase(forbiddenNames[n], nameLen)) {
+      // invalid name is either the entire string, or a prefix with a period
+      if (aText.Length() == nameLen || aText.CharAt(nameLen) == char16_t('.')) {
+        aText.Truncate();
+        break;
+      }
+    }
+  }
+}
+
+//
 // Given a unicode string, convert it down to a valid local charset filename
 // with the supplied extension. This ensures that we do not cut MBCS characters
 // in the middle.
 //
 // It would seem that this is more functionality suited to being in nsIFile.
 //
 static bool CreateFilenameFromTextA(nsString& aText, const char* aExtension,
                                     char* aFilename, uint32_t aFilenameLen) {
   // ensure that the supplied name doesn't have invalid characters. If
   // a valid mangled filename couldn't be created then it will leave the
   // text empty.
-  nsLocalFile::CheckForReservedFileName(aText);
+  MangleTextToValidFilename(aText);
   if (aText.IsEmpty()) return false;
 
   // repeatably call WideCharToMultiByte as long as the title doesn't fit in the
   // buffer available to us. Continually reduce the length of the source title
   // until the MBCS version will fit in the buffer with room for the supplied
   // extension. Doing it this way ensures that even in MBCS environments there
   // will be a valid MBCS filename of the correct length.
   int maxUsableFilenameLen =
@@ -1127,17 +1156,17 @@ static bool CreateFilenameFromTextA(nsSt
   }
 }
 
 static bool CreateFilenameFromTextW(nsString& aText, const wchar_t* aExtension,
                                     wchar_t* aFilename, uint32_t aFilenameLen) {
   // ensure that the supplied name doesn't have invalid characters. If
   // a valid mangled filename couldn't be created then it will leave the
   // text empty.
-  nsLocalFile::CheckForReservedFileName(aText);
+  MangleTextToValidFilename(aText);
   if (aText.IsEmpty()) return false;
 
   const int extensionLen = wcslen(aExtension);
   if (aText.Length() + extensionLen + 1 > aFilenameLen)
     aText.Truncate(aFilenameLen - extensionLen - 1);
   wcscpy(&aFilename[0], aText.get());
   wcscpy(&aFilename[aText.Length()], aExtension);
   return true;
@@ -2149,17 +2178,17 @@ HRESULT nsDataObj::GetDownloadDetails(ns
     nsAutoCString urlFileName;
     sourceURL->GetFileName(urlFileName);
     NS_UnescapeURL(urlFileName);
     CopyUTF8toUTF16(urlFileName, srcFileName);
   }
   if (srcFileName.IsEmpty()) return E_FAIL;
 
   // make the name safe for the filesystem
-  nsLocalFile::CheckForReservedFileName(srcFileName);
+  MangleTextToValidFilename(srcFileName);
 
   sourceURI.swap(*aSourceURI);
   aFilename = srcFileName;
   return S_OK;
 }
 
 HRESULT nsDataObj::GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG) {
   HGLOBAL fileGroupDescHandle =
--- a/xpcom/base/nsCRTGlue.h
+++ b/xpcom/base/nsCRTGlue.h
@@ -124,26 +124,19 @@ void NS_MakeRandomString(char* aBuf, int
 
 #define FF '\f'
 #define TAB '\t'
 
 #define CRSTR "\015"
 #define LFSTR "\012"
 #define CRLF "\015\012" /* A CR LF equivalent string */
 
-#if defined(ANDROID)
-// On mobile devices, the file system may be very limited in what it
-// considers valid characters. To avoid errors, sanitize conservatively.
-#  define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|;,+=[]"
-#else
-// Otherwise, we use the most restrictive filesystem as our default set of
-// illegal filename characters. This is currently Windows.
-#  define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|"
-#endif
-
+// We use the most restrictive filesystem as our default set of illegal filename
+// characters. This is currently Windows.
+#define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|"
 // We also provide a list of all known file path separators for all filesystems.
 // This can be used in replacement of FILE_PATH_SEPARATOR when you need to
 // identify or replace all known path separators.
 #define KNOWN_PATH_SEPARATORS "\\/"
 
 #if defined(XP_MACOSX)
 #  define FILE_PATH_SEPARATOR "/"
 #elif defined(XP_WIN)
@@ -155,18 +148,13 @@ void NS_MakeRandomString(char* aBuf, int
 #endif
 
 // Not all these control characters are illegal in all OSs, but we don't really
 // want them appearing in filenames
 #define CONTROL_CHARACTERS           \
   "\001\002\003\004\005\006\007"     \
   "\010\011\012\013\014\015\016\017" \
   "\020\021\022\023\024\025\026\027" \
-  "\030\031\032\033\034\035\036\037" \
-  "\177"                             \
-  "\200\201\202\203\204\205\206\207" \
-  "\210\211\212\213\214\215\216\217" \
-  "\220\221\222\223\224\225\226\227" \
-  "\230\231\232\233\234\235\236\237"
+  "\030\031\032\033\034\035\036\037"
 
 #define FILE_ILLEGAL_CHARACTERS CONTROL_CHARACTERS OS_FILE_ILLEGAL_CHARACTERS
 
 #endif  // nsCRTGlue_h__
--- a/xpcom/io/nsLocalFileWin.cpp
+++ b/xpcom/io/nsLocalFileWin.cpp
@@ -186,36 +186,16 @@ nsresult nsLocalFile::RevealFile(const n
 
     CoTaskMemFree(dir);
     CoTaskMemFree(item);
   }
 
   return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
 }
 
-// static
-void nsLocalFile::CheckForReservedFileName(nsString& aFileName) {
-  static const char* forbiddenNames[] = {
-      "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",  "COM8",
-      "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6",  "LPT7",
-      "LPT8", "LPT9", "CON",  "PRN",  "AUX",  "NUL",  "CLOCK$"};
-
-  uint32_t nameLen;
-  for (size_t n = 0; n < ArrayLength(forbiddenNames); ++n) {
-    nameLen = (uint32_t)strlen(forbiddenNames[n]);
-    if (aFileName.EqualsIgnoreCase(forbiddenNames[n], nameLen)) {
-      // invalid name is either the entire string, or a prefix with a period
-      if (aFileName.Length() == nameLen ||
-          aFileName.CharAt(nameLen) == char16_t('.')) {
-        aFileName.Truncate();
-      }
-    }
-  }
-}
-
 class nsDriveEnumerator : public nsSimpleEnumerator,
                           public nsIDirectoryEnumerator {
  public:
   explicit nsDriveEnumerator(bool aUseDOSDevicePathSyntax);
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSISIMPLEENUMERATOR
   NS_FORWARD_NSISIMPLEENUMERATORBASE(nsSimpleEnumerator::)
   nsresult Init();
--- a/xpcom/io/nsLocalFileWin.h
+++ b/xpcom/io/nsLocalFileWin.h
@@ -44,20 +44,16 @@ class nsLocalFile final : public nsILoca
 
  public:
   // Removes registry command handler parameters, quotes, and expands
   // environment strings.
   static bool CleanupCmdHandlerPath(nsAString& aCommandHandler);
   // Called off the main thread to open the window revealing the file
   static nsresult RevealFile(const nsString& aResolvedPath);
 
-  // Checks if the filename is one of the windows reserved filenames
-  // (com1, com2, etc...) and truncates the string if so.
-  static void CheckForReservedFileName(nsString& aFileName);
-
  private:
   // CopyMove and CopySingleFile constants for |options| parameter:
   enum CopyFileOption {
     FollowSymlinks = 1u << 0,
     Move = 1u << 1,
     SkipNtfsAclReset = 1u << 2,
     Rename = 1u << 3
   };