mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
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:
parent
1a03e480e1
commit
61f7e59993
@ -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);
|
||||
|
110
docshell/test/mochitest/file_bug1747033.sjs
Normal file
110
docshell/test/mochitest/file_bug1747033.sjs
Normal 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();
|
||||
}
|
@ -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
|
||||
|
97
docshell/test/mochitest/test_bug1747033.html
Normal file
97
docshell/test/mochitest/test_bug1747033.html
Normal 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>
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -44,6 +44,7 @@ struct HttpChannelOnStartRequestArgs
|
||||
ResourceTimingStructArgs timing;
|
||||
bool allRedirectsSameOrigin;
|
||||
uint32_t? multiPartID;
|
||||
bool isFirstPartOfMultiPart;
|
||||
bool isLastPartOfMultiPart;
|
||||
CrossOriginOpenerPolicy openerPolicy;
|
||||
nsIReferrerInfo overrideReferrerInfo;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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};
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user