<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Ensure fragment is kept across redirects</title>
<meta name="timeout" content="long">
<link rel=help href="https://www.w3.org/TR/cuap/#uri">
<link rel=help href="https://tools.ietf.org/html/rfc7231#section-7.1.2">
<link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=158420">
<link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=24175">
<script src="/common/get-host-info.sub.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let frame;
let message;
const HTTP_SAME_ORIGIN = "HTTP - SameOrigin";
const HTTPS_SAME_ORIGIN = "HTTPS - SameOrigin";
const HTTP_CROSS_ORIGIN = "HTTP - CrossOrigin";
const HTTPS_CROSS_ORIGIN = "HTTPS - CrossOrigin";
function messageReceived(f) {
return new Promise((resolve) => {
window.addEventListener("message", (e) => {
message = e.data;
resolve();
}, {once: true});
f();
});
}
function getHostname(navigation_type) {
switch (navigation_type) {
case HTTP_SAME_ORIGIN:
return get_host_info().HTTP_ORIGIN;
case HTTPS_SAME_ORIGIN:
return get_host_info().HTTPS_ORIGIN
case HTTP_CROSS_ORIGIN:
return get_host_info().HTTP_REMOTE_ORIGIN
case HTTPS_CROSS_ORIGIN:
return get_host_info().HTTPS_REMOTE_ORIGIN
}
return 'nonexistent'
}
// Turns |path| from a relative to this file path into a full URL, with
// the host being determined by one of the ORIGIN strings above.
function relativePathToFull(path, navigation_type) {
let host = getHostname(navigation_type);
const pathname = window.location.pathname;
const base_path = pathname.substring(0, pathname.lastIndexOf('/') + 1);
return host + base_path + path;
}
// Constructs a URL to redirect.py which will respond with the given
// redirect status |code| to the provided |to_url|. Optionally adds on a
// |fragment|, if provided, to use in the initial request to redirect.py
function buildRedirectUrl(to_url, code, fragment) {
to_url = encodeURIComponent(to_url);
let dest = `/common/redirect.py?status=${code}&location=${to_url}`;
if (fragment)
dest = dest + '#' + fragment;
return dest;
}
async function redirectTo(url, code, navigation_type, fragment) {
const dest = buildRedirectUrl(url, code, fragment);
await messageReceived( () => {
frame.contentWindow.location = getHostname(navigation_type) + dest;
});
}
async function doubleRedirectTo(url, code, navigation_type, fragment, intermediate_fragment) {
const second_redirection = buildRedirectUrl(url, code, intermediate_fragment);
const first_redirection = buildRedirectUrl(second_redirection, code, fragment);
await messageReceived( () => {
frame.contentWindow.location = getHostname(navigation_type) + first_redirection;
});
}
onload = () => {
frame = document.getElementById("frame");
// The tests in this file verify fragments are correctly propagated in
// a number of HTTP redirect scenarios. Each test is run for every
// relevant redirect status code. We also run each scenario under each
// combination of navigating to cross/same origin and using http/https.
const status_codes = [301, 302, 303, 307, 308];
const navigation_types = [HTTP_SAME_ORIGIN,
HTTPS_SAME_ORIGIN,
HTTP_CROSS_ORIGIN,
HTTPS_CROSS_ORIGIN];
for (let navigation_type of navigation_types) {
// Navigate to a URL with a fragment. The URL redirects to a different
// page. Ensure we land on the redirected page with the fragment
// specified in the initial navigation's URL.
//
// Redirect chain: urlA#target -> urlB
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html', navigation_type);
await redirectTo(to_url, code, navigation_type, "target");
assert_true(message.url.endsWith('#target'));
assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation.");
}, `[${navigation_type}] Preserve fragment in ${code} redirect`);
}
// Navigate to a URL with a fragment. The URL redirects to a different
// URL that also contains a fragment. Ensure we land on the redirected
// page using the fragment specified in the redirect response and not
// the one in the initial navigation.
//
// Redirect chain: urlA#target -> urlB#fromRedirect
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
await redirectTo(to_url, code, navigation_type, "target");
assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
}, `[${navigation_type}] Redirect URL fragment takes precedence in ${code} redirect`);
}
// Perform two redirects. The initial navigation has a fragment and
// will redirect to a URL that also responds with a redirect. Ensure we
// land on the final page with the fragment from the original
// navigation.
//
// Redirect chain: urlA#target -> urlB -> urlC
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html', navigation_type);
await doubleRedirectTo(to_url, code, navigation_type, "target");
assert_true(message.url.endsWith('#target'), `Unexpected fragment: ${message.url}`);
assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation.");
}, `[${navigation_type}] Preserve fragment in multiple ${code} redirects`);
}
// Perform two redirects. The initial navigation has a fragment and
// will redirect to a URL that also responds with a redirect. The
// second redirection to the final page also has a fragment. Ensure we
// land on the final page with the fragment from the redirection
// response URL.
//
// Redirect chain: urlA#target -> urlB -> urlC#fromRedirect
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
await doubleRedirectTo(to_url, code, navigation_type, "target");
assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
}, `[${navigation_type}] Destination URL fragment takes precedence in multiple ${code} redirects`);
}
// Perform two redirects. The initial navigation has a fragment and
// will redirect to a URL that also responds with a redirect. This
// time, both redirect response have a fragment. Ensure we land on the
// final page with the fragment from the last redirection response URL.
//
// Redirect chain: urlA#target -> urlB#intermediate -> urlC#fromRedirect
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
await doubleRedirectTo(to_url, code, navigation_type, "target", "intermediate");
assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
}, `[${navigation_type}] Final redirect fragment takes precedence over intermediate in multiple ${code} redirects`);
}
// Perform two redirects. The initial navigation has a fragment and
// will redirect to a URL that also responds with a redirect. The first
// redirect response has a fragment but the second doesn't. Ensure we
// land on the final page with the fragment from the first redirection
// response URL.
//
// Redirect chain: urlA#target -> urlB#fromRedirect -> urlC
//
for (let code of status_codes) {
promise_test(async () => {
const to_url = relativePathToFull('resources/destination.html', navigation_type);
await doubleRedirectTo(to_url, code, navigation_type, "target", "fromRedirect");
assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
}, `[${navigation_type}] Preserve intermediate fragment in multiple ${code} redirects`);
}
}
}
</script>
</head>
<body>
<iframe id="frame" src=""></iframe>
</body>
</html>