Bug 1971140 - Improve Content-Disposition: attachment handling for object/embed, r=smaug a=dmeehan

Differential Revision: https://phabricator.services.mozilla.com/D253075
This commit is contained in:
Nika Layzell
2025-06-10 17:13:39 +00:00
committed by dmeehan@mozilla.com
parent 2b7bee41af
commit 7bfb0e8915
11 changed files with 177 additions and 145 deletions

View File

@@ -10795,11 +10795,14 @@ static nsresult AppendSegmentToString(nsIInputStream* aIn, void* aClosure,
openFlags |= nsIURILoader::DONT_RETARGET;
}
// Unless the pref is set, object/embed loads always specify DONT_RETARGET.
// See bug 1868001 for details.
if (!aIsDocumentLoad &&
!StaticPrefs::dom_navigation_object_embed_allow_retargeting()) {
openFlags |= nsIURILoader::DONT_RETARGET;
if (!aIsDocumentLoad) {
openFlags |= nsIURILoader::IS_OBJECT_EMBED;
// Unless the pref is set, object/embed loads always specify DONT_RETARGET.
// See bug 1868001 for details.
if (!StaticPrefs::dom_navigation_object_embed_allow_retargeting()) {
openFlags |= nsIURILoader::DONT_RETARGET;
}
}
return openFlags;
@@ -10890,8 +10893,14 @@ nsresult nsDocShell::OpenRedirectedChannel(nsDocShellLoadState* aLoadState) {
// ClientInfo, so we just need to allocate a corresponding ClientSource.
CreateReservedSourceIfNeeded(channel, GetMainThreadSerialEventTarget());
uint32_t documentOpenInfoFlags = nsIURILoader::DONT_RETARGET;
if (loadInfo->GetExternalContentPolicyType() ==
ExtContentPolicy::TYPE_OBJECT) {
documentOpenInfoFlags |= nsIURILoader::IS_OBJECT_EMBED;
}
RefPtr<nsDocumentOpenInfo> loader =
new nsDocumentOpenInfo(this, nsIURILoader::DONT_RETARGET, nullptr);
new nsDocumentOpenInfo(this, documentOpenInfoFlags, nullptr);
channel->SetLoadGroup(mLoadGroup);
MOZ_ALWAYS_SUCCEEDS(loader->Prepare());

View File

@@ -123,6 +123,12 @@ support-files = [
"file_pdf_object_attachment.html",
"file_pdf_attachment.pdf",
"file_pdf_attachment.pdf^headers^",
"file_svg_object_attachment.html",
"file_svg_attachment.svg",
"file_svg_attachment.svg^headers^",
"file_html_object_attachment.html",
"file_html_attachment.html",
"file_html_attachment.html^headers^",
]
["browser_outline_refocus.js"]

View File

@@ -7,29 +7,75 @@ const httpsTestRoot = getRootDirectory(gTestPath).replace(
"https://example.com"
);
add_task(async function test_pdf_object_attachment() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", false]],
});
async function loadAndCheck(file, displayInline, downloadFile = null) {
// Get the downloads list and add a view to listen for a download to be added.
// We do this even if we aren't going to download anything, so we notice if a
// download is started.
let download;
let downloadList = await Downloads.getList(Downloads.ALL);
let downloadView = {
async onDownloadAdded(aDownload) {
info("download added");
ok(downloadFile, "Should be expecting a download");
download = aDownload;
// Clean up the download from the list
downloadList.remove(aDownload);
await aDownload.finalize(true);
},
};
await downloadList.addView(downloadView);
// Open the new URL and perform the load.
await BrowserTestUtils.withNewTab(
`${httpsTestRoot}/file_pdf_object_attachment.html`,
`${httpsTestRoot}/${file}`,
async browser => {
is(
browser.browsingContext.children.length,
1,
"Should have a child frame"
displayInline ? 1 : 0,
`Should ${displayInline ? "not " : ""}have a child frame`
);
await SpecialPowers.spawn(
browser,
[displayInline],
async displayInline => {
let obj = content.document.querySelector("object");
is(
obj.displayedType,
displayInline
? Ci.nsIObjectLoadingContent.TYPE_DOCUMENT
: Ci.nsIObjectLoadingContent.TYPE_FALLBACK,
`should be displaying TYPE_${displayInline ? "DOCUMENT" : "FALLBACK"}`
);
}
);
await SpecialPowers.spawn(browser, [], async () => {
let obj = content.document.querySelector("object");
is(
obj.displayedType,
Ci.nsIObjectLoadingContent.TYPE_DOCUMENT,
"should be displaying TYPE_DOCUMENT"
);
});
}
);
// Clean up our download observer.
await downloadList.removeView(downloadView);
if (downloadFile) {
is(
download.source.url,
`${httpsTestRoot}/${downloadFile}`,
"Download has the correct source"
);
} else {
is(download, undefined, "Should not have seen a download");
}
}
add_task(async function test_pdf_object_attachment() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.navigation.object_embed.allow_retargeting", false],
["browser.download.open_pdf_attachments_inline", false],
],
});
// PDF attachment should display inline.
await loadAndCheck("file_pdf_object_attachment.html", true);
});
add_task(async function test_img_object_attachment() {
@@ -37,132 +83,71 @@ add_task(async function test_img_object_attachment() {
set: [["dom.navigation.object_embed.allow_retargeting", false]],
});
await BrowserTestUtils.withNewTab(
`${httpsTestRoot}/file_img_object_attachment.html`,
async browser => {
is(
browser.browsingContext.children.length,
1,
"Should have a child frame"
);
await SpecialPowers.spawn(browser, [], async () => {
let obj = content.document.querySelector("object");
is(
obj.displayedType,
Ci.nsIObjectLoadingContent.TYPE_DOCUMENT,
"should be displaying TYPE_DOCUMENT"
);
});
}
);
// Image attachment should display inline.
await loadAndCheck("file_img_object_attachment.html", true);
});
async function waitForDownload() {
// Get the downloads list and add a view to listen for a download to be added.
let downloadList = await Downloads.getList(Downloads.ALL);
// Wait for a single download
let downloadView;
let finishedAllDownloads = new Promise(resolve => {
downloadView = {
onDownloadAdded(aDownload) {
info("download added");
resolve(aDownload);
},
};
add_task(async function test_svg_object_attachment() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", false]],
});
await downloadList.addView(downloadView);
let download = await finishedAllDownloads;
await downloadList.removeView(downloadView);
// Clean up the download from the list.
await downloadList.remove(download);
await download.finalize(true);
// SVG attachment should fail to load.
await loadAndCheck("file_svg_object_attachment.html", false);
});
// Return the download
return download;
}
add_task(async function test_html_object_attachment() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", false]],
});
add_task(async function test_pdf_object_attachment_download() {
// HTML attachment should fail to load.
await loadAndCheck("file_html_object_attachment.html", false);
});
add_task(async function test_pdf_object_attachment_allow_retargeting() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.navigation.object_embed.allow_retargeting", true],
["browser.download.open_pdf_attachments_inline", false],
],
});
// Even if `allow_retargeting` is enabled, we always display PDFs inline.
await loadAndCheck("file_pdf_object_attachment.html", true);
});
add_task(async function test_img_object_attachment_allow_retargeting() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", true]],
});
// Set the behaviour to save pdfs to disk and not handle internally, so we
// don't end up with extra tabs after the test.
var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
Ci.nsIHandlerService
);
const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
let previousAction = mimeInfo.preferredAction;
mimeInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
gHandlerSvc.store(mimeInfo);
registerCleanupFunction(() => {
mimeInfo.preferredAction = previousAction;
gHandlerSvc.store(mimeInfo);
});
// Start listening for the download before opening the new tab.
let downloadPromise = waitForDownload();
await BrowserTestUtils.withNewTab(
`${httpsTestRoot}/file_pdf_object_attachment.html`,
async browser => {
let download = await downloadPromise;
is(
download.source.url,
`${httpsTestRoot}/file_pdf_attachment.pdf`,
"download should be the pdf"
);
await SpecialPowers.spawn(browser, [], async () => {
let obj = content.document.querySelector("object");
is(
obj.displayedType,
Ci.nsIObjectLoadingContent.TYPE_FALLBACK,
"should be displaying TYPE_FALLBACK"
);
});
}
);
// Even if `allow_retargeting` is enabled, we always display images inline.
await loadAndCheck("file_img_object_attachment.html", true);
});
add_task(async function test_img_object_attachment_download() {
// NOTE: This is testing our current behaviour here as of bug 1868001 (which
// is to download an image with `Content-Disposition: attachment` embedded
// within an object or embed element).
//
// Other browsers ignore the `Content-Disposition: attachment` header when
// loading images within object or embed element as-of december 2023, as
// we did prior to the changes in bug 1595491.
//
// If this turns out to be a web-compat issue, we may want to introduce
// special handling to ignore content-disposition when loading images within
// an object or embed element.
add_task(async function test_svg_object_attachment_allow_retargeting() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", true]],
});
// Start listening for the download before opening the new tab.
let downloadPromise = waitForDownload();
await BrowserTestUtils.withNewTab(
`${httpsTestRoot}/file_img_object_attachment.html`,
async browser => {
let download = await downloadPromise;
is(
download.source.url,
`${httpsTestRoot}/file_img_attachment.jpg`,
"download should be the jpg"
);
await SpecialPowers.spawn(browser, [], async () => {
let obj = content.document.querySelector("object");
is(
obj.displayedType,
Ci.nsIObjectLoadingContent.TYPE_FALLBACK,
"should be displaying TYPE_FALLBACK"
);
});
}
// SVG attachments are downloaded if allow_retargeting is set.
await loadAndCheck(
"file_svg_object_attachment.html",
false,
"file_svg_attachment.svg"
);
});
add_task(async function test_html_object_attachment_allow_retargeting() {
await SpecialPowers.pushPrefEnv({
set: [["dom.navigation.object_embed.allow_retargeting", true]],
});
// HTML attachments are downloaded if allow_retargeting is set.
await loadAndCheck(
"file_html_object_attachment.html",
false,
"file_html_attachment.html"
);
});

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>

View File

@@ -0,0 +1,2 @@
Content-Type: text/html
Content-Disposition: attachment

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<object data="file_html_attachment.html" width="100%" height="600">fallback</object>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500px" height="500px">
<rect x1="0" y1="0" width="100%" height="100%" fill="lime"/>
</svg>

After

Width:  |  Height:  |  Size: 140 B

View File

@@ -0,0 +1,2 @@
Content-Type: text/svg+xml
Content-Disposition: attachment

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<object data="file_svg_attachment.svg" width="100%" height="600">fallback</object>
</body>
</html>

View File

@@ -53,6 +53,12 @@ interface nsIURILoader : nsISupports
* be indicated.
*/
const unsigned long DONT_RETARGET = 1 << 1;
/**
* If this flag is set, the navigation is for an object or embed element,
* which may handle some content types internally, even if an attachment
* content disposition is specified.
*/
const unsigned long IS_OBJECT_EMBED = 1 << 2;
/* @} */
/**

View File

@@ -49,6 +49,7 @@
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_general.h"
#include "nsContentUtils.h"
#include "imgLoader.h"
mozilla::LazyLogModule nsURILoader::mLog("URILoader");
@@ -409,26 +410,26 @@ nsresult nsDocumentOpenInfo::DispatchContent(nsIRequest* request) {
// could happen because the Content-Disposition header is set so, or, in the
// future, because the user has specified external handling for the MIME
// type.
//
// If we're not going to be able to retarget to an external handler, ignore
// content-disposition, and unconditionally try to display the content.
// This is used for object/embed tags, which expect to display subresources
// marked with an attachment disposition.
bool forceExternalHandling = false;
if (!(mFlags & nsIURILoader::DONT_RETARGET)) {
uint32_t disposition;
rv = aChannel->GetContentDisposition(&disposition);
uint32_t disposition;
rv = aChannel->GetContentDisposition(&disposition);
if (NS_SUCCEEDED(rv) && disposition == nsIChannel::DISPOSITION_ATTACHMENT) {
forceExternalHandling = true;
}
}
bool forceExternalHandling =
NS_SUCCEEDED(rv) && disposition == nsIChannel::DISPOSITION_ATTACHMENT;
LOG((" forceExternalHandling: %s", forceExternalHandling ? "yes" : "no"));
LOG((" IsSandboxed: %s", IsSandboxed(aChannel) ? "yes" : "no"));
LOG((" IsContentPDF: %s",
IsContentPDF(aChannel, mContentType) ? "yes" : "no"));
// Ignore the Content-Disposition header if we're loading a PDF or Image
// subresource within an object/embed element.
if (forceExternalHandling && (mFlags & nsIURILoader::IS_OBJECT_EMBED) &&
(imgLoader::SupportImageWithMimeType(mContentType) ||
IsContentPDF(aChannel, mContentType))) {
LOG(("Handling pdf/image MIME internally for object/embed element"));
forceExternalHandling = false;
}
bool maybeForceInternalHandling =
forceExternalHandling &&
mozilla::StaticPrefs::browser_download_open_pdf_attachments_inline();