blob: d1214ab015cdaf68d59ed7b21c88f031571fc2be [file] [edit]
<!doctype html>
<meta charset="utf-8" />
<title>
User activation: bless() works in nested cross-origin / cross-site iframes
</title>
<meta name="timeout" content="long" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<body></body>
<script type="module">
// "sameOrigin" is the top's own origin; the cross-origins are distinct cross-site hosts via .sub
// substitution (get_host_info() can't be used: from localhost its remote origins collapse to one).
const ORIGINS = {
sameOrigin: location.origin,
crossOriginA: "http://{{hosts[][]}}:{{ports[http][0]}}",
crossOriginB: "http://{{hosts[alt][]}}:{{ports[http][0]}}",
crossOriginC: "http://{{hosts[alt][www1]}}:{{ports[http][0]}}",
};
const RELAY = new URL("resources/bless-relay.sub.html", location.href).pathname;
test(() => {
for (const [name, origin] of Object.entries(ORIGINS))
assert_false(origin.includes("{{"), `origin ${name} substituted (${origin})`);
assert_equals(new Set(Object.values(ORIGINS)).size, 4, "the four origins are distinct");
}, "the test server provides four distinct origins");
let counter = 0;
const nextId = () => "case" + ++counter;
function frameAtDepth(rootFrame, depth) {
let frame = rootFrame.contentWindow;
for (let level = 1; level < depth; level++) frame = frame.frames[0];
return frame;
}
// Dispatch same-id messages (parsed) to handler; returns an unlisten function.
function listen(id, handler) {
function onMessage(event) {
if (typeof event.data !== "string") return;
let message;
try { message = JSON.parse(event.data); } catch { return; }
if (message.id === id) handler(message);
}
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}
function buildChain(tokens, id) {
const { promise, resolve, reject } = Promise.withResolvers();
const ready = new Set();
let unlisten, timer;
const stop = () => { clearTimeout(timer); unlisten(); };
timer = step_timeout(() => {
stop();
reject(new Error(`build timed out: ${ready.size}/${tokens.length} frames ready`));
}, 8000);
unlisten = listen(id, (message) => {
if (message.type === "error") {
stop();
reject(new Error(`depth ${message.depth}: ${message.reason}`));
} else if (message.type === "ready") {
ready.add(message.depth);
if (ready.size === tokens.length) { stop(); resolve(iframe); }
}
});
const descendantOrigins = tokens.slice(1).map((token) => ORIGINS[token]);
const iframe = document.createElement("iframe");
// Offset from top-left so the click must be resolved to top-window coordinates.
iframe.style = "position:absolute; left:40px; top:40px; width:700px; height:500px;";
iframe.src = ORIGINS[tokens[0]] + RELAY +
`?id=${id}&depth=1&chain=${encodeURIComponent(JSON.stringify(descendantOrigins))}`;
document.body.appendChild(iframe);
return promise;
}
function command(rootFrame, id, depth, cmd, replyType) {
const { promise, resolve, reject } = Promise.withResolvers();
let unlisten, timer;
timer = step_timeout(() => {
unlisten();
reject(new Error(`${cmd} at depth ${depth} timed out`));
}, 8000);
unlisten = listen(id, (message) => {
if (message.depth === depth && message.type === replyType) {
clearTimeout(timer);
unlisten();
resolve(message);
}
});
frameAtDepth(rootFrame, depth).postMessage(
JSON.stringify({ id, targetDepth: depth, cmd }), "*");
return promise;
}
// Sticky hasBeenActive per frame (never expires, so robust to slow round-trips); top omitted.
async function activationVector(rootFrame, id, depth) {
const activations = [];
for (let frameDepth = 1; frameDepth <= depth; frameDepth++)
activations.push((await command(rootFrame, id, frameDepth, "report", "state")).hasBeenActive);
return activations;
}
function expectedActivationVector(tokens, blessAt) {
const origins = [ORIGINS.sameOrigin, ...tokens.map((token) => ORIGINS[token])];
const blessedOrigin = origins[blessAt];
return origins
.map((origin, depth) => depth <= blessAt || origin === blessedOrigin)
.slice(1);
}
function blessInChain(tokens, blessAt, name) {
promise_test(async (t) => {
const id = nextId();
const rootFrame = await buildChain(tokens, id);
t.add_cleanup(() => rootFrame.remove());
const blessed = await command(rootFrame, id, blessAt, "bless", "done");
assert_true(blessed.ok, `bless resolved (${blessed.error || ""})`);
assert_true(blessed.isActive, "blessed frame has transient activation");
assert_array_equals(
await activationVector(rootFrame, id, tokens.length),
expectedActivationVector(tokens, blessAt),
`frames activated for [${tokens.join(", ")}] blessing depth ${blessAt}`);
}, name);
}
// [chain of iframe origins (top-down), depth to bless, test name]
const CASES = [
[["sameOrigin"], 1, "bless in a same-origin iframe"],
[["crossOriginA"], 1, "bless in a cross-origin iframe"],
[["sameOrigin", "sameOrigin"], 2, "bless two same-origin iframes deep"],
[["sameOrigin", "crossOriginA"], 2, "bless in a cross-origin iframe inside a same-origin iframe"],
[["crossOriginA", "sameOrigin"], 2, "bless in a same-origin iframe inside a cross-origin iframe"],
[["crossOriginA", "crossOriginA"], 2, "bless in a cross-origin iframe inside a cross-origin iframe"],
[["crossOriginA", "crossOriginB"], 2, "bless in a cross-origin iframe inside a different cross-origin iframe"],
[["sameOrigin", "crossOriginA"], 1, "blessing a same-origin iframe does not activate its cross-origin child"],
[["crossOriginA", "crossOriginB"], 1, "blessing a cross-origin iframe does not activate its cross-origin child"],
[["sameOrigin", "crossOriginA", "sameOrigin"], 3, "bless in a same-origin iframe nested behind a cross-origin iframe"],
// Currently FAILs: WebKit over-activates the nested same-origin frame when the cross-origin
// frame is clicked (webkit.org/b/318749). The expectation is spec-correct; the committed
// baseline captures the failure until that bug is fixed.
[["sameOrigin", "crossOriginA", "sameOrigin"], 2, "blessing a cross-origin iframe does not activate the same-origin iframe nested below it"],
[["sameOrigin", "crossOriginA", "sameOrigin"], 1, "blessing the outer iframe activates a same-origin descendant through a cross-origin frame, but not the cross-origin frame itself"],
[["crossOriginA", "crossOriginB", "crossOriginC"], 3, "bless three distinct cross-origins deep"],
[["crossOriginA", "crossOriginB", "crossOriginC"], 1, "blessing a cross-origin iframe does not activate its distinct cross-origin descendants"],
[["sameOrigin", "crossOriginA", "crossOriginB"], 3, "bless in a cross-origin iframe nested behind a same-origin and a cross-origin iframe"],
[["sameOrigin", "sameOrigin", "sameOrigin"], 3, "bless three same-origin iframes deep"],
];
for (const [tokens, blessAt, name] of CASES) blessInChain(tokens, blessAt, name);
</script>