Bug 1747033 - "Back" does not work correctly for pages with multipart/x-mixed-replace and history.replaceState. r=smaug,necko-reviewers,valentin

multipart/x-mixed-replace loads don't start a new load for parts other than the first,
they just call OnStartRequest/OnStopRequest for every part. The nsDocShell code was
trying to set the active entry to the loading entry for these loads, but because we
never started a new load after the first part, the loading entry would be null and we'd
just clear the active entry. history.replaceState would then try to replace the active
entry, but finding none it would just add a new one.

Differential Revision: https://phabricator.services.mozilla.com/D138464
This commit is contained in:
Peter Van der Beken 2022-02-19 08:30:35 +00:00
parent 1a03e480e1
commit 61f7e59993
12 changed files with 266 additions and 11 deletions

View File

@ -5636,6 +5636,14 @@ nsresult nsDocShell::RefreshURIFromQueue() {
return NS_OK;
}
static bool IsFollowupPartOfMultipart(nsIRequest* aRequest) {
nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
bool firstPart = false;
return multiPartChannel &&
NS_SUCCEEDED(multiPartChannel->GetIsFirstPart(&firstPart)) &&
!firstPart;
}
nsresult nsDocShell::Embed(nsIContentViewer* aContentViewer,
WindowGlobalChild* aWindowActor,
bool aIsTransientAboutBlank, bool aPersist,
@ -5663,7 +5671,8 @@ nsresult nsDocShell::Embed(nsIContentViewer* aContentViewer,
SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE));
}
if (!aIsTransientAboutBlank && mozilla::SessionHistoryInParent()) {
if (!aIsTransientAboutBlank && mozilla::SessionHistoryInParent() &&
!IsFollowupPartOfMultipart(aRequest)) {
bool expired = false;
uint32_t cacheKey = 0;
nsCOMPtr<nsICacheInfoChannel> cacheChannel = do_QueryInterface(aRequest);

View File

@ -0,0 +1,110 @@
"use strict";
const BOUNDARY = "BOUNDARY";
// waitForPageShow should be false if this is for multipart/x-mixed-replace
// and it's not the last part, Gecko doesn't fire pageshow for those parts.
function documentString(waitForPageShow = true) {
return `<html>
<head>
<script>
let bc = new BroadcastChannel("bug1747033");
bc.addEventListener("message", ({ data: { cmd, arg = undefined } }) => {
switch (cmd) {
case "load":
location.href = arg;
break;
case "replaceState":
history.replaceState({}, "Replaced state", arg);
bc.postMessage({ "historyLength": history.length, "location": location.href });
break;
case "back":
history.back();
break;
case "close":
close();
break;
}
});
function reply() {
bc.postMessage({ "historyLength": history.length, "location": location.href });
}
${waitForPageShow ? `addEventListener("pageshow", reply);` : "reply();"}
</script>
</head>
<body></body>
</html>
`;
}
function boundary(last = false) {
let b = `--${BOUNDARY}`;
if (last) {
b += "--";
}
return b + "\n";
}
function sendMultipart(response, last = false) {
setState("sendMore", "");
response.write(`Content-Type: text/html
${documentString(last)}
`);
response.write(boundary(last));
}
function shouldSendMore() {
return new Promise(resolve => {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(
() => {
let sendMore = getState("sendMore");
if (sendMore !== "") {
timer.cancel();
resolve(sendMore);
}
},
100,
Ci.nsITimer.TYPE_REPEATING_SLACK
);
});
}
async function handleRequest(request, response) {
if (request.queryString == "") {
// This is for non-multipart/x-mixed-replace loads.
response.write(documentString());
return;
}
if (request.queryString == "sendNextPart") {
setState("sendMore", "next");
return;
}
if (request.queryString == "sendLastPart") {
setState("sendMore", "last");
return;
}
response.processAsync();
response.setHeader(
"Content-Type",
`multipart/x-mixed-replace; boundary=${BOUNDARY}`,
false
);
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(boundary());
sendMultipart(response);
while ((await shouldSendMore("sendMore")) !== "last") {
sendMultipart(response);
}
sendMultipart(response, true);
response.finish();
}

View File

@ -109,6 +109,10 @@ support-files = file_bug675587.html
[test_bug1151421.html]
[test_bug1186774.html]
[test_bug1450164.html]
[test_bug1507702.html]
[test_bug1645781.html]
support-files =
form_submit.sjs
[test_bug1729662.html]
support-files =
file_bug1729662.html
@ -133,6 +137,9 @@ skip-if = toolkit == "android" && debug && fission && verify # Bug 1745937
[test_bug1743353.html]
support-files =
file_bug1743353.html
[test_bug1747033.html]
support-files =
file_bug1747033.sjs
[test_close_onpagehide_by_history_back.html]
[test_close_onpagehide_by_window_close.html]
[test_compressed_multipart.html]
@ -152,10 +159,6 @@ support-files =
[test_windowedhistoryframes.html]
skip-if = (!debug && os == 'android') # Bug 1573892
[test_triggeringprincipal_location_seturi.html]
[test_bug1507702.html]
[test_bug1645781.html]
support-files =
form_submit.sjs
[test_double_submit.html]
support-files =
clicker.html

View File

@ -0,0 +1,97 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test history after loading multipart</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<script>
SimpleTest.waitForExplicitFinish();
function runTest() {
let bc = new BroadcastChannel("bug1747033");
new Promise(resolve => {
bc.addEventListener("message", ({ data: { historyLength } }) => {
is(historyLength, 1, "Correct length for first normal load.");
resolve();
}, { once: true });
window.open("file_bug1747033.sjs", "", "noopener");
}).then(() => {
return new Promise(resolve => {
let loaded = 0;
bc.addEventListener("message", function listener({ data: { historyLength } }) {
++loaded;
is(historyLength, 2, `Correct length for multipart load ${loaded}.`);
// We want 3 parts in total.
if (loaded < 3) {
if (loaded == 2) {
// We've had 2 parts, make the server send the last part.
fetch("file_bug1747033.sjs?sendLastPart");
} else {
fetch("file_bug1747033.sjs?sendNextPart");
}
return;
}
bc.removeEventListener("message", listener);
resolve();
});
bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs?multipart" });
});
}).then(() => {
return new Promise(resolve => {
bc.addEventListener("message", ({ data: { historyLength } }) => {
is(historyLength, 2, "Correct length after calling replaceState in multipart.");
resolve();
}, { once: true });
bc.postMessage({ cmd: "replaceState", arg: "file_bug1747033.sjs?replaced" });
});
}).then(() => {
return new Promise(resolve => {
bc.addEventListener("message", ({ data: { historyLength } }) => {
is(historyLength, 3, "Correct length for first normal load after multipart.");
resolve();
}, { once: true });
bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs" });
});
}).then(() => {
return new Promise(resolve => {
let goneBack = 0;
bc.addEventListener("message", function listener({ data: { historyLength } }) {
++goneBack;
is(historyLength, 3, "Correct length after going back.");
if (goneBack == 1) {
bc.postMessage({ cmd: "back" });
} else if (goneBack == 2) {
bc.removeEventListener("message", listener);
resolve();
}
});
bc.postMessage({ cmd: "back" });
});
}).then(() => {
bc.postMessage({ cmd: "close" });
SimpleTest.finish();
});
}
</script>
</head>
<body onload="runTest();">
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>

View File

@ -12,7 +12,7 @@ interface nsIChannel;
* associated with a MultiPartChannel.
*/
[scriptable, uuid(4fefb490-5567-11e5-a837-0800200c9a66)]
[scriptable, builtinclass, uuid(4fefb490-5567-11e5-a837-0800200c9a66)]
interface nsIMultiPartChannel : nsISupports
{
/**
@ -26,6 +26,8 @@ interface nsIMultiPartChannel : nsISupports
*/
readonly attribute uint32_t partID;
[noscript] readonly attribute boolean isFirstPart;
/**
* Set to true when onStopRequest is received from the base channel.
* The listener can check this from its onStopRequest to determine

View File

@ -84,6 +84,7 @@ HttpChannelChild::HttpChannelChild()
mKeptAlive(false),
mIPCActorDeleted(false),
mSuspendSent(false),
mIsFirstPartOfMultiPart(false),
mIsLastPartOfMultiPart(false),
mSuspendForWaitCompleteRedirectSetup(false),
mRecvOnStartRequestSentCalled(false),
@ -460,6 +461,7 @@ void HttpChannelChild::OnStartRequest(
StoreAllRedirectsSameOrigin(aArgs.allRedirectsSameOrigin());
mMultiPartID = aArgs.multiPartID();
mIsFirstPartOfMultiPart = aArgs.isFirstPartOfMultiPart();
mIsLastPartOfMultiPart = aArgs.isLastPartOfMultiPart();
if (aArgs.overrideReferrerInfo()) {
@ -2689,6 +2691,15 @@ HttpChannelChild::GetPartID(uint32_t* aPartID) {
return NS_OK;
}
NS_IMETHODIMP
HttpChannelChild::GetIsFirstPart(bool* aIsFirstPart) {
if (!mMultiPartID) {
return NS_ERROR_NOT_AVAILABLE;
}
*aIsFirstPart = mIsFirstPartOfMultiPart;
return NS_OK;
}
NS_IMETHODIMP
HttpChannelChild::GetIsLastPart(bool* aIsLastPart) {
if (!mMultiPartID) {

View File

@ -359,6 +359,10 @@ class HttpChannelChild final : public PHttpChannelChild,
// diverting callbacks to parent.
uint8_t mSuspendSent : 1;
// True if this channel is a multi-part channel, and the first part
// is currently being processed.
uint8_t mIsFirstPartOfMultiPart : 1;
// True if this channel is a multi-part channel, and the last part
// is currently being processed.
uint8_t mIsLastPartOfMultiPart : 1;

View File

@ -44,6 +44,7 @@ struct HttpChannelOnStartRequestArgs
ResourceTimingStructArgs timing;
bool allRedirectsSameOrigin;
uint32_t? multiPartID;
bool isFirstPartOfMultiPart;
bool isLastPartOfMultiPart;
CrossOriginOpenerPolicy openerPolicy;
nsIReferrerInfo overrideReferrerInfo;

View File

@ -1036,6 +1036,7 @@ HttpChannelParent::OnStartRequest(nsIRequest* aRequest) {
MOZ_ASSERT(NS_IsMainThread());
Maybe<uint32_t> multiPartID;
bool isFirstPartOfMultiPart = false;
bool isLastPartOfMultiPart = false;
DebugOnly<bool> isMultiPart = false;
@ -1051,6 +1052,7 @@ HttpChannelParent::OnStartRequest(nsIRequest* aRequest) {
uint32_t partID = 0;
multiPartChannel->GetPartID(&partID);
multiPartID = Some(partID);
multiPartChannel->GetIsFirstPart(&isFirstPartOfMultiPart);
multiPartChannel->GetIsLastPart(&isLastPartOfMultiPart);
} else if (nsCOMPtr<nsIViewSourceChannel> viewSourceChannel =
do_QueryInterface(aRequest)) {
@ -1088,6 +1090,7 @@ HttpChannelParent::OnStartRequest(nsIRequest* aRequest) {
}
args.multiPartID() = multiPartID;
args.isFirstPartOfMultiPart() = isFirstPartOfMultiPart;
args.isLastPartOfMultiPart() = isLastPartOfMultiPart;
args.cacheExpirationTime() = nsICacheEntry::NO_EXPIRATION_TIME;

View File

@ -26,10 +26,11 @@
using namespace mozilla;
nsPartChannel::nsPartChannel(nsIChannel* aMultipartChannel, uint32_t aPartID,
nsIStreamListener* aListener)
bool aIsFirstPart, nsIStreamListener* aListener)
: mMultipartChannel(aMultipartChannel),
mListener(aListener),
mPartID(aPartID) {
mPartID(aPartID),
mIsFirstPart(aIsFirstPart) {
// Inherit the load flags from the original channel...
mMultipartChannel->GetLoadFlags(&mLoadFlags);
@ -343,6 +344,12 @@ nsPartChannel::GetPartID(uint32_t* aPartID) {
return NS_OK;
}
NS_IMETHODIMP
nsPartChannel::GetIsFirstPart(bool* aIsFirstPart) {
*aIsFirstPart = mIsFirstPart;
return NS_OK;
}
NS_IMETHODIMP
nsPartChannel::GetIsLastPart(bool* aIsLastPart) {
*aIsLastPart = mIsLastPart;
@ -797,8 +804,10 @@ nsresult nsMultiMixedConv::SendStart() {
MOZ_ASSERT(!mPartChannel, "tisk tisk, shouldn't be overwriting a channel");
nsPartChannel* newChannel;
newChannel = new nsPartChannel(mChannel, mCurrentPartID++, partListener);
if (!newChannel) return NS_ERROR_OUT_OF_MEMORY;
newChannel = new nsPartChannel(mChannel, mCurrentPartID, mCurrentPartID == 0,
partListener);
++mCurrentPartID;
if (mIsByteRangeRequest) {
newChannel->InitializeByteRange(mByteRangeStart, mByteRangeEnd);

View File

@ -35,7 +35,7 @@ class nsPartChannel final : public nsIChannel,
public nsIMultiPartChannel {
public:
nsPartChannel(nsIChannel* aMultipartChannel, uint32_t aPartID,
nsIStreamListener* aListener);
bool aIsFirstPart, nsIStreamListener* aListener);
void InitializeByteRange(int64_t aStart, int64_t aEnd);
void SetIsLastPart() { mIsLastPart = true; }
@ -83,6 +83,7 @@ class nsPartChannel final : public nsIChannel,
uint32_t mPartID; // unique ID that can be used to identify
// this part of the multipart document
bool mIsFirstPart;
bool mIsLastPart{false};
};

View File

@ -437,6 +437,11 @@ ExternalHelperAppParent::GetPartID(uint32_t* aPartID) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
ExternalHelperAppParent::GetIsFirstPart(bool* aIsLastPart) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
ExternalHelperAppParent::GetIsLastPart(bool* aIsLastPart) {
return NS_ERROR_NOT_IMPLEMENTED;