Bug 1526731 - pass content policy to webbrowserpersist to improve image request headers, r=smaug,johannh
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Thu, 23 Jan 2020 08:36:00 +0000
changeset 511323 af1b9cf9eec60af9cebc46a90b393f1ef8313d43
parent 511322 2840946503b2b78ee175d121371991a3ef234df2
child 511324 169c76460aa48f861d4c8f916d3aacdd40238f1a
push id37048
push userrmaries@mozilla.com
push dateThu, 23 Jan 2020 21:42:24 +0000
treeherdermozilla-central@fb6b61e49217 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, johannh
bugs1526731
milestone74.0a1
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
Bug 1526731 - pass content policy to webbrowserpersist to improve image request headers, r=smaug,johannh Differential Revision: https://phabricator.services.mozilla.com/D60567
browser/actors/ContextMenuChild.jsm
browser/base/content/nsContextMenu.js
browser/components/shell/nsMacShellService.cpp
dom/base/nsContentAreaDragDrop.cpp
dom/base/nsContentAreaDragDrop.h
dom/tests/browser/browser.ini
dom/tests/browser/browser_persist_image_accept.js
dom/tests/browser/browser_persist_mixed_content_image.js
dom/webbrowserpersist/nsIWebBrowserPersist.idl
dom/webbrowserpersist/nsWebBrowserPersist.cpp
toolkit/components/browser/nsWebBrowser.cpp
toolkit/components/downloads/test/unit/head.js
toolkit/components/viewsource/content/viewSourceUtils.js
toolkit/content/contentAreaUtils.js
toolkit/content/tests/browser/browser_saveImageURL.js
--- a/browser/actors/ContextMenuChild.jsm
+++ b/browser/actors/ContextMenuChild.jsm
@@ -218,16 +218,18 @@ class ContextMenuChild extends JSWindowA
           "canvas"
         );
         canvas.width = video.videoWidth;
         canvas.height = video.videoHeight;
 
         let ctxDraw = canvas.getContext("2d");
         ctxDraw.drawImage(video, 0, 0);
 
+        // Note: if changing the content type, don't forget to update
+        // consumers that also hardcode this content type.
         return Promise.resolve(canvas.toDataURL("image/jpeg", ""));
       }
 
       case "ContextMenu:SetAsDesktopBackground": {
         let target = ContentDOMReference.resolve(message.data.targetIdentifier);
 
         // Paranoia: check disableSetDesktopBackground again, in case the
         // image changed since the context menu was initiated.
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1280,17 +1280,17 @@ class nsContextMenu {
       saveImageURL(
         dataURL,
         name,
         "SaveImageTitle",
         true, // bypass cache
         false, // don't skip prompt for where to save
         referrerInfo, // referrer info
         null, // document
-        null, // content type
+        "image/jpeg", // content type - keep in sync with ContextMenuChild!
         null, // content disposition
         isPrivate,
         this.principal
       );
     });
   }
 
   leaveDOMFullScreen() {
@@ -1588,17 +1588,17 @@ class nsContextMenu {
         saveImageURL(
           blobURL,
           "canvas.png",
           "SaveImageTitle",
           true,
           false,
           referrerInfo,
           null,
-          null,
+          "image/png", // _canvasToBlobURL uses image/png by default.
           null,
           isPrivate,
           document.nodePrincipal /* system, because blob: */
         );
       }, Cu.reportError);
     } else if (this.onImage) {
       urlSecurityCheck(this.mediaURL, this.principal);
       saveImageURL(
--- a/browser/components/shell/nsMacShellService.cpp
+++ b/browser/components/shell/nsMacShellService.cpp
@@ -148,17 +148,18 @@ nsMacShellService::SetDesktopBackground(
   if (docShell) {
     loadContext = do_QueryInterface(docShell);
   }
 
   nsCOMPtr<nsIReferrerInfo> referrerInfo = new mozilla::dom::ReferrerInfo();
   referrerInfo->InitWithNode(aElement);
 
   return wbp->SaveURI(imageURI, aElement->NodePrincipal(), 0, referrerInfo,
-                      nullptr, nullptr, mBackgroundFile, loadContext);
+                      nullptr, nullptr, mBackgroundFile,
+                      nsIContentPolicy::TYPE_IMAGE, loadContext);
 }
 
 NS_IMETHODIMP
 nsMacShellService::OnProgressChange(nsIWebProgress* aWebProgress,
                                     nsIRequest* aRequest,
                                     int32_t aCurSelfProgress,
                                     int32_t aMaxSelfProgress,
                                     int32_t aCurTotalProgress,
--- a/dom/base/nsContentAreaDragDrop.cpp
+++ b/dom/base/nsContentAreaDragDrop.cpp
@@ -23,16 +23,17 @@
 #include "nsISupportsPrimitives.h"
 #include "nsServiceManagerUtils.h"
 #include "nsNetUtil.h"
 #include "nsIFile.h"
 #include "nsFrameLoader.h"
 #include "nsFrameLoaderOwner.h"
 #include "nsIContent.h"
 #include "nsIContentInlines.h"
+#include "nsIContentPolicy.h"
 #include "nsIImageLoadingContent.h"
 #include "nsUnicharUtils.h"
 #include "nsIURL.h"
 #include "nsIURIMutator.h"
 #include "mozilla/dom/Document.h"
 #include "nsIPrincipal.h"
 #include "nsIWebBrowserPersist.h"
 #include "nsEscape.h"
@@ -122,17 +123,18 @@ nsresult nsContentAreaDragDrop::GetDragD
 
 NS_IMPL_ISUPPORTS(nsContentAreaDragDropDataProvider, nsIFlavorDataProvider)
 
 // SaveURIToFile
 // used on platforms where it's possible to drag items (e.g. images)
 // into the file system
 nsresult nsContentAreaDragDropDataProvider::SaveURIToFile(
     nsIURI* inSourceURI, nsIPrincipal* inTriggeringPrincipal,
-    nsIFile* inDestFile, bool isPrivate) {
+    nsIFile* inDestFile, nsContentPolicyType inContentPolicyType,
+    bool isPrivate) {
   nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(inSourceURI);
   if (!sourceURL) {
     return NS_ERROR_NO_INTERFACE;
   }
 
   nsresult rv = inDestFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -143,17 +145,17 @@ nsresult nsContentAreaDragDropDataProvid
   NS_ENSURE_SUCCESS(rv, rv);
 
   persist->SetPersistFlags(
       nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION);
 
   // referrer policy can be anything since the referrer is nullptr
   return persist->SavePrivacyAwareURI(inSourceURI, inTriggeringPrincipal, 0,
                                       nullptr, nullptr, nullptr, inDestFile,
-                                      isPrivate);
+                                      inContentPolicyType, isPrivate);
 }
 
 /*
  * Check if the provided filename extension is valid for the MIME type and
  * return the MIME type's primary extension.
  *
  * @param aExtension           [in]  the extension to check
  * @param aMimeType            [in]  the MIME type to check the extension with
@@ -308,17 +310,20 @@ nsContentAreaDragDropDataProvider::GetFl
     rv = destDirectory->Clone(getter_AddRefs(file));
     NS_ENSURE_SUCCESS(rv, rv);
 
     file->Append(targetFilename);
 
     bool isPrivate = aTransferable->GetIsPrivateData();
 
     nsCOMPtr<nsIPrincipal> principal = aTransferable->GetRequestingPrincipal();
-    rv = SaveURIToFile(sourceURI, principal, file, isPrivate);
+    nsContentPolicyType contentPolicyType =
+        aTransferable->GetContentPolicyType();
+    rv =
+        SaveURIToFile(sourceURI, principal, file, contentPolicyType, isPrivate);
     // send back an nsIFile
     if (NS_SUCCEEDED(rv)) {
       CallQueryInterface(file, aData);
     }
   }
 
   return rv;
 }
--- a/dom/base/nsContentAreaDragDrop.h
+++ b/dom/base/nsContentAreaDragDrop.h
@@ -68,12 +68,13 @@ class nsContentAreaDragDropDataProvider 
   virtual ~nsContentAreaDragDropDataProvider() {}
 
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIFLAVORDATAPROVIDER
 
   nsresult SaveURIToFile(nsIURI* inSourceURI,
                          nsIPrincipal* inTriggeringPrincipal,
-                         nsIFile* inDestFile, bool isPrivate);
+                         nsIFile* inDestFile, nsContentPolicyType inPolicyType,
+                         bool isPrivate);
 };
 
 #endif /* nsContentAreaDragDrop_h__ */
--- a/dom/tests/browser/browser.ini
+++ b/dom/tests/browser/browser.ini
@@ -67,16 +67,17 @@ fail-if = fission
 skip-if =
   !e10s || # This is a test of e10s functionality.
   (fission && debug) # Intermittently fails uncleanly and breaks subsequent tests.
 [browser_persist_cookies.js]
 support-files =
   set-samesite-cookies-and-redirect.sjs
   mimeme.sjs
 skip-if = fission
+[browser_persist_image_accept.js]
 [browser_persist_mixed_content_image.js]
 support-files =
   test_mixed_content_image.html
   dummy.png
 [browser_pointerlock_warning.js]
 [browser_test_focus_after_modal_state.js]
 skip-if = verify
 support-files =
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_persist_image_accept.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content",
+  "https://example.org"
+);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+registerCleanupFunction(async function() {
+  info("Running the cleanup code");
+  MockFilePicker.cleanup();
+  if (gTestDir && gTestDir.exists()) {
+    // On Windows, sometimes nsIFile.remove() throws, probably because we're
+    // still writing to the directory we're trying to remove, despite
+    // waiting for the download to complete. Just retry a bit later...
+    let succeeded = false;
+    while (!succeeded) {
+      try {
+        gTestDir.remove(true);
+        succeeded = true;
+      } catch (ex) {
+        await new Promise(requestAnimationFrame);
+      }
+    }
+  }
+});
+
+let gTestDir = null;
+
+function createTemporarySaveDirectory() {
+  var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+  saveDir.append("testsavedir");
+  if (!saveDir.exists()) {
+    info("create testsavedir!");
+    saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+  }
+  info("return from createTempSaveDir: " + saveDir.path);
+  return saveDir;
+}
+
+add_task(async function test_image_download() {
+  await BrowserTestUtils.withNewTab(TEST_PATH + "dummy.html", async browser => {
+    // Add the image, and wait for it to load.
+    await SpecialPowers.spawn(browser, [], async function() {
+      let loc = content.document.location.href;
+      let imgloc = new content.URL("dummy.png", loc);
+      let img = content.document.createElement("img");
+      img.src = imgloc;
+      await new Promise(resolve => {
+        img.onload = resolve;
+        content.document.body.appendChild(img);
+      });
+    });
+    gTestDir = createTemporarySaveDirectory();
+
+    let destFile = gTestDir.clone();
+
+    MockFilePicker.displayDirectory = gTestDir;
+    let fileName;
+    MockFilePicker.showCallback = function(fp) {
+      info("showCallback");
+      fileName = fp.defaultString;
+      info("fileName: " + fileName);
+      destFile.append(fileName);
+      info("path: " + destFile.path);
+      MockFilePicker.setFiles([destFile]);
+      MockFilePicker.filterIndex = 0; // just save the file
+      info("done showCallback");
+    };
+    let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+    let downloadFinishedPromise = new Promise(resolve => {
+      publicDownloads.addView({
+        onDownloadChanged(download) {
+          info("Download changed!");
+          if (download.succeeded || download.error) {
+            info("Download succeeded or errored");
+            publicDownloads.removeView(this);
+            publicDownloads.removeFinished();
+            resolve(download);
+          }
+        },
+      });
+    });
+    let httpOnModifyPromise = TestUtils.topicObserved(
+      "http-on-modify-request",
+      (s, t, d) => {
+        let channel = s.QueryInterface(Ci.nsIChannel);
+        let uri = channel.URI && channel.URI.spec;
+        if (!uri.endsWith("dummy.png")) {
+          info("Ignoring request for " + uri);
+          return false;
+        }
+        ok(channel instanceof Ci.nsIHttpChannel, "Should be HTTP channel");
+        channel.QueryInterface(Ci.nsIHttpChannel);
+        is(
+          channel.getRequestHeader("Accept"),
+          Services.prefs.getCharPref("image.http.accept"),
+          "Header should be image header"
+        );
+        return true;
+      }
+    );
+    // open the context menu.
+    let popup = document.getElementById("contentAreaContextMenu");
+    let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+    BrowserTestUtils.synthesizeMouseAtCenter(
+      "img",
+      { type: "contextmenu", button: 2 },
+      browser
+    );
+    await popupShown;
+    let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+    popup.querySelector("#context-saveimage").click();
+    popup.hidePopup();
+    await popupHidden;
+    info("Context menu hidden, waiting for download to finish");
+    let imageDownload = await downloadFinishedPromise;
+    ok(imageDownload.succeeded, "Image should have downloaded successfully");
+    info("Waiting for http request to complete.");
+    // Ensure we got the http request:
+    await httpOnModifyPromise;
+  });
+});
--- a/dom/tests/browser/browser_persist_mixed_content_image.js
+++ b/dom/tests/browser/browser_persist_mixed_content_image.js
@@ -70,25 +70,25 @@ add_task(async function test_image_downl
         fileName = fp.defaultString;
         info("fileName: " + fileName);
         destFile.append(fileName);
         info("path: " + destFile.path);
         MockFilePicker.setFiles([destFile]);
         MockFilePicker.filterIndex = 0; // just save the file
         info("done showCallback");
       };
-      let downloadFinishedPromise = new Promise(async resolve => {
-        let dls = await Downloads.getList(Downloads.PUBLIC);
-        dls.addView({
+      let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+      let downloadFinishedPromise = new Promise(resolve => {
+        publicDownloads.addView({
           onDownloadChanged(download) {
             info("Download changed!");
             if (download.succeeded || download.error) {
               info("Download succeeded or errored");
-              dls.removeView(this);
-              dls.removeFinished();
+              publicDownloads.removeView(this);
+              publicDownloads.removeFinished();
               resolve(download);
             }
           },
         });
       });
       // open the context menu.
       let popup = document.getElementById("contentAreaContextMenu");
       let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
--- a/dom/webbrowserpersist/nsIWebBrowserPersist.idl
+++ b/dom/webbrowserpersist/nsIWebBrowserPersist.idl
@@ -1,15 +1,16 @@
 /* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * 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 "nsICancelable.idl"
+#include "nsIContentPolicy.idl"
 
 interface nsIURI;
 interface nsIInputStream;
 interface nsIWebProgressListener;
 interface nsIFile;
 interface nsIChannel;
 interface nsILoadContext;
 interface nsIPrincipal;
@@ -119,16 +120,17 @@ interface nsIWebBrowserPersist : nsICanc
    *                   HTTP Referer header.
    * @param aPostData  Post data to pass with an HTTP request or
    *                   <CODE>nullptr</CODE>.
    * @param aExtraHeaders Additional headers to supply with an HTTP request
    *                   or <CODE>nullptr</CODE>.
    * @param aFile      Target file. This may be a nsIFile object or an
    *                   nsIURI object with a file scheme or a scheme that
    *                   supports uploading (e.g. ftp).
+   * @param aContentPolicyType The type of content we're saving.
    * @param aPrivacyContext A context from which the privacy status of this
    *                   save operation can be determined. Must only be null
    *                   in situations in which no such context is available
    *                   (eg. the operation has no logical association with any
    *                   window or document)
    *
    * @see nsIFile
    * @see nsIURI
@@ -136,29 +138,31 @@ interface nsIWebBrowserPersist : nsICanc
    *
    * @throws NS_ERROR_INVALID_ARG One or more arguments was invalid.
    */
   void saveURI(in nsIURI aURI, in nsIPrincipal aTriggeringPrincipal,
       in unsigned long aCacheKey,
       in nsIReferrerInfo aReferrerInfo,
       in nsIInputStream aPostData,
       in string aExtraHeaders, in nsISupports aFile,
+      in nsContentPolicyType aContentPolicyType,
       in nsILoadContext aPrivacyContext);
 
   /**
    * @param aIsPrivate Treat the save operation as private (ie. with
    *                   regards to networking operations and persistence
    *                   of intermediate data, etc.)
    * @see saveURI for all other parameter descriptions
    */
   void savePrivacyAwareURI(in nsIURI aURI,
       in nsIPrincipal aTriggeringPrincipal, in unsigned long aCacheKey,
       in nsIReferrerInfo aReferrerInfo,
       in nsIInputStream aPostData,
       in string aExtraHeaders, in nsISupports aFile,
+      in nsContentPolicyType aContentPolicyType,
       in boolean aIsPrivate);
 
   /**
    * Save a channel to a file. It must not be opened yet.
    * @see saveURI
    */
   void saveChannel(in nsIChannel aChannel, in nsISupports aFile);
 
--- a/dom/webbrowserpersist/nsWebBrowserPersist.cpp
+++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp
@@ -354,39 +354,41 @@ NS_IMETHODIMP nsWebBrowserPersist::SetPr
   mEventSink = do_GetInterface(aProgressListener);
   return NS_OK;
 }
 
 NS_IMETHODIMP nsWebBrowserPersist::SaveURI(
     nsIURI* aURI, nsIPrincipal* aPrincipal, uint32_t aCacheKey,
     nsIReferrerInfo* aReferrerInfo, nsIInputStream* aPostData,
     const char* aExtraHeaders, nsISupports* aFile,
-    nsILoadContext* aPrivacyContext) {
+    nsContentPolicyType aContentPolicyType, nsILoadContext* aPrivacyContext) {
   bool isPrivate = aPrivacyContext && aPrivacyContext->UsePrivateBrowsing();
   return SavePrivacyAwareURI(aURI, aPrincipal, aCacheKey, aReferrerInfo,
-                             aPostData, aExtraHeaders, aFile, isPrivate);
+                             aPostData, aExtraHeaders, aFile,
+                             aContentPolicyType, isPrivate);
 }
 
 NS_IMETHODIMP nsWebBrowserPersist::SavePrivacyAwareURI(
     nsIURI* aURI, nsIPrincipal* aPrincipal, uint32_t aCacheKey,
     nsIReferrerInfo* aReferrerInfo, nsIInputStream* aPostData,
-    const char* aExtraHeaders, nsISupports* aFile, bool aIsPrivate) {
+    const char* aExtraHeaders, nsISupports* aFile,
+    nsContentPolicyType aContentPolicy, bool aIsPrivate) {
   NS_ENSURE_TRUE(mFirstAndOnlyUse, NS_ERROR_FAILURE);
   mFirstAndOnlyUse = false;  // Stop people from reusing this object!
 
   nsCOMPtr<nsIURI> fileAsURI;
   nsresult rv;
   rv = GetValidURIFromObject(aFile, getter_AddRefs(fileAsURI));
   NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG);
 
   // SaveURI doesn't like broken uris.
   mPersistFlags |= PERSIST_FLAGS_FAIL_ON_BROKEN_LINKS;
-  rv = SaveURIInternal(aURI, aPrincipal, nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD,
-                       aCacheKey, aReferrerInfo, aPostData, aExtraHeaders,
-                       fileAsURI, false, aIsPrivate);
+  rv = SaveURIInternal(aURI, aPrincipal, aContentPolicy, aCacheKey,
+                       aReferrerInfo, aPostData, aExtraHeaders, fileAsURI,
+                       false, aIsPrivate);
   return NS_FAILED(rv) ? rv : NS_OK;
 }
 
 NS_IMETHODIMP nsWebBrowserPersist::SaveChannel(nsIChannel* aChannel,
                                                nsISupports* aFile) {
   NS_ENSURE_TRUE(mFirstAndOnlyUse, NS_ERROR_FAILURE);
   mFirstAndOnlyUse = false;  // Stop people from reusing this object!
 
--- a/toolkit/components/browser/nsWebBrowser.cpp
+++ b/toolkit/components/browser/nsWebBrowser.cpp
@@ -728,28 +728,32 @@ nsWebBrowser::SetProgressListener(nsIWeb
   mProgressListener = aProgressListener;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsWebBrowser::SaveURI(nsIURI* aURI, nsIPrincipal* aPrincipal,
                       uint32_t aCacheKey, nsIReferrerInfo* aReferrerInfo,
                       nsIInputStream* aPostData, const char* aExtraHeaders,
-                      nsISupports* aFile, nsILoadContext* aPrivacyContext) {
+                      nsISupports* aFile,
+                      nsContentPolicyType aContentPolicyType,
+                      nsILoadContext* aPrivacyContext) {
   return SavePrivacyAwareURI(
       aURI, aPrincipal, aCacheKey, aReferrerInfo, aPostData, aExtraHeaders,
-      aFile, aPrivacyContext && aPrivacyContext->UsePrivateBrowsing());
+      aFile, aContentPolicyType,
+      aPrivacyContext && aPrivacyContext->UsePrivateBrowsing());
 }
 
 NS_IMETHODIMP
 nsWebBrowser::SavePrivacyAwareURI(nsIURI* aURI, nsIPrincipal* aPrincipal,
                                   uint32_t aCacheKey,
                                   nsIReferrerInfo* aReferrerInfo,
                                   nsIInputStream* aPostData,
                                   const char* aExtraHeaders, nsISupports* aFile,
+                                  nsContentPolicyType aContentPolicyType,
                                   bool aIsPrivate) {
   if (mPersist) {
     uint32_t currentState;
     mPersist->GetCurrentState(&currentState);
     if (currentState == PERSIST_STATE_FINISHED) {
       mPersist = nullptr;
     } else {
       // You can't save again until the last save has completed
@@ -772,17 +776,17 @@ nsWebBrowser::SavePrivacyAwareURI(nsIURI
   mPersist = do_CreateInstance(NS_WEBBROWSERPERSIST_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
   mPersist->SetProgressListener(this);
   mPersist->SetPersistFlags(mPersistFlags);
   mPersist->GetCurrentState(&mPersistCurrentState);
 
   rv = mPersist->SavePrivacyAwareURI(uri, aPrincipal, aCacheKey, aReferrerInfo,
                                      aPostData, aExtraHeaders, aFile,
-                                     aIsPrivate);
+                                     aContentPolicyType, aIsPrivate);
   if (NS_FAILED(rv)) {
     mPersist = nullptr;
   }
   return rv;
 }
 
 NS_IMETHODIMP
 nsWebBrowser::SaveChannel(nsIChannel* aChannel, nsISupports* aFile) {
--- a/toolkit/components/downloads/test/unit/head.js
+++ b/toolkit/components/downloads/test/unit/head.js
@@ -415,16 +415,17 @@ function promiseStartLegacyDownload(aSou
         persist.savePrivacyAwareURI(
           sourceURI,
           Services.scriptSecurityManager.getSystemPrincipal(),
           0,
           referrerInfo,
           null,
           null,
           targetFile,
+          Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
           isPrivate
         );
       })
       .catch(do_report_unexpected_exception);
   });
 }
 
 /**
--- a/toolkit/components/viewsource/content/viewSourceUtils.js
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -249,16 +249,17 @@ var gViewSourceUtils = {
           webBrowserPersist.savePrivacyAwareURI(
             uri,
             principal,
             null,
             null,
             null,
             null,
             file,
+            Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
             data.isPrivate
           );
 
           let helperService = Cc[
             "@mozilla.org/uriloader/external-helper-app-service;1"
           ].getService(Ci.nsPIExternalAppLauncher);
           if (data.isPrivate) {
             // register the file to be deleted when possible
--- a/toolkit/content/contentAreaUtils.js
+++ b/toolkit/content/contentAreaUtils.js
@@ -461,16 +461,17 @@ function internalSave(
   if (aCacheKey == undefined) {
     aCacheKey = 0;
   }
 
   // Note: aDocument == null when this code is used by save-link-as...
   var saveMode = GetSaveModeForContentType(aContentType, aDocument);
 
   var file, sourceURI, saveAsType;
+  let contentPolicyType = Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD;
   // Find the URI object for aURL and the FileName/Extension to use when saving.
   // FileName/Extension will be ignored if aChosenData supplied.
   if (aChosenData) {
     file = aChosenData.file;
     sourceURI = aChosenData.uri;
     saveAsType = kSaveAsType_Complete;
 
     continueSave();
@@ -485,16 +486,19 @@ function internalSave(
       aURL,
       charset,
       aDocument,
       aContentType,
       aContentDisposition
     );
     sourceURI = fileInfo.uri;
 
+    if (aContentType && aContentType.startsWith("image/")) {
+      contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE;
+    }
     var fpParams = {
       fpTitleKey: aFilePickerTitleKey,
       fileInfo,
       contentType: aContentType,
       saveMode,
       saveAsType: kSaveAsType_Complete,
       file,
     };
@@ -553,16 +557,17 @@ function internalSave(
       sourcePrincipal,
       sourceReferrerInfo: aReferrerInfo,
       sourceDocument: useSaveDocument ? aDocument : null,
       targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
       targetFile: file,
       sourceCacheKey: aCacheKey,
       sourcePostData: nonCPOWDocument ? getPostData(aDocument) : null,
       bypassCache: aShouldBypassCache,
+      contentPolicyType,
       isPrivate,
     };
 
     // Start the actual save process
     internalPersist(persistArgs);
   }
 }
 
@@ -581,16 +586,19 @@ function internalSave(
  *        the nsIReferrerInfo of the referrer info to use, or null if no
  *        referrer should be sent.
  * @param persistArgs.sourcePostData
  *        Required and used only when persistArgs.sourceDocument is NOT present,
  *        represents the POST data to be sent along with the HTTP request, and
  *        must be null if no POST data should be sent.
  * @param persistArgs.targetFile
  *        The nsIFile of the file to create
+ * @param persistArgs.contentPolicyType
+ *        The type of content we're saving. Will be used to determine what
+ *        content is accepted, enforce sniffing restrictions, etc.
  * @param persistArgs.targetContentType
  *        Required and used only when persistArgs.sourceDocument is present,
  *        determines the final content type of the saved file, or null to use
  *        the same content type as the source document. Currently only
  *        "text/plain" is meaningful.
  * @param persistArgs.bypassCache
  *        If true, the document will always be refetched from the server
  * @param persistArgs.isPrivate
@@ -666,16 +674,17 @@ function internalPersist(persistArgs) {
     persist.savePrivacyAwareURI(
       persistArgs.sourceURI,
       persistArgs.sourcePrincipal,
       persistArgs.sourceCacheKey,
       persistArgs.sourceReferrerInfo,
       persistArgs.sourcePostData,
       null,
       targetFileURL,
+      persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
       persistArgs.isPrivate
     );
   }
 }
 
 /**
  * Structure for holding info about automatically supplied parameters for
  * internalSave(...). This allows parameters to be supplied so the user does not
--- a/toolkit/content/tests/browser/browser_saveImageURL.js
+++ b/toolkit/content/tests/browser/browser_saveImageURL.js
@@ -43,17 +43,17 @@ add_task(async function preferred_API() 
       saveImageURL(
         url,
         "image.jpg",
         null,
         true,
         false,
         null,
         null,
-        null,
+        "image/jpeg",
         null,
         false,
         gBrowser.contentPrincipal
       );
       await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
         let channel = docShell.currentDocumentChannel;
         if (channel) {
           todo(
@@ -109,13 +109,23 @@ add_task(async function deprecated_API()
           // Throttleable is the only class flag assigned to downloads.
           todo(
             channel.QueryInterface(Ci.nsIClassOfService).classFlags ==
               Ci.nsIClassOfService.Throttleable
           );
         }
       });
       let filePickerPromise = waitForFilePicker();
-      saveImageURL(url, "image.jpg", null, true, false, null, doc, null, null);
+      saveImageURL(
+        url,
+        "image.jpg",
+        null,
+        true,
+        false,
+        null,
+        doc,
+        "image/jpeg",
+        null
+      );
       await filePickerPromise;
     }
   );
 });