LibWeb: Reuse initial about:blank window for first same-origin load
Follow the spec reuse of the initial same-origin about:blank window for
a browsing context's first real navigation. This fixes a crash in
promise-job-entry-different-function-realm.html by preserving the
correct iframe realm.
However, reusing the initial about:blank Window means
create-and-initialize can associate that Window with the pending
Document before session history activation has made the Document active.
Treating a browsing context's active document as its active Window's
associated Document therefore exposes the pending Document too early.
To fix this, add an explicit active document slot to BrowsingContext and
update it when a Document is made active.
A similar fix was attempted in 7fc7263a4d,
but that version still queued navigation tasks through the reused active
Window, tagging them with the pending Document before it was active.
This caused parser-created iframe loads to hang in encoding WPTs. This
commit instead queues those navigation-internal tasks against the
navigable's currently active Document.
The newly added iframe-initial-load-chunked-body.html mimics the same
type of failure seen by the WPT regressions mentioned above.
This commit is contained in:
parent
edaed9adfa
commit
2ae54d77e0
17 changed files with 241 additions and 20 deletions
|
|
@ -344,15 +344,11 @@ WebIDL::ExceptionOr<GC::Ref<Document>> Document::create_and_initialize(Type type
|
|||
// 6. If browsingContext's active document's is initial about:blank is true,
|
||||
// and browsingContext's active document's origin is same origin-domain with navigationParams's origin,
|
||||
// then set window to browsingContext's active window.
|
||||
// FIXME: still_on_its_initial_about_blank_document() is not in the spec anymore.
|
||||
// However, replacing this with the spec-mandated is_initial_about_blank() results in the browsing context
|
||||
// holding an incorrect active document for the replace from initial about:blank to the real document.
|
||||
// See #22293 for more details.
|
||||
if (false
|
||||
&& (browsing_context->active_document() && browsing_context->active_document()->origin().is_same_origin(navigation_params.origin))) {
|
||||
VERIFY(browsing_context->active_document());
|
||||
if (browsing_context->active_document()->is_initial_about_blank()
|
||||
&& browsing_context->active_document()->origin().is_same_origin_domain(navigation_params.origin)) {
|
||||
window = browsing_context->active_window();
|
||||
}
|
||||
|
||||
// 7. Otherwise:
|
||||
else {
|
||||
// FIXME: 1. Let oacHeader be the result of getting a structured field value given `Origin-Agent-Cluster` and "item" from response's header list.
|
||||
|
|
@ -5631,6 +5627,12 @@ void Document::make_active()
|
|||
|
||||
set_window(window);
|
||||
|
||||
// AD-HOC: Keep the browsing context's active document distinct from the Window's associated Document.
|
||||
// The associated Document can be updated during document creation, but the browsing context
|
||||
// should only expose the new Document once it is made active.
|
||||
// Spec issue: https://github.com/whatwg/html/issues/12415
|
||||
m_browsing_context->set_active_document(*this);
|
||||
|
||||
// 2. Set document's browsing context's WindowProxy's [[Window]] internal slot value to window.
|
||||
m_browsing_context->window_proxy()->set_window(window);
|
||||
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ void BrowsingContext::visit_edges(Cell::Visitor& visitor)
|
|||
|
||||
visitor.visit(m_page);
|
||||
visitor.visit(m_window_proxy);
|
||||
visitor.visit(m_active_document);
|
||||
visitor.visit(m_group);
|
||||
visitor.visit(m_opener_browsing_context);
|
||||
}
|
||||
|
|
@ -347,21 +348,24 @@ GC::Ptr<BrowsingContext> BrowsingContext::top_level_browsing_context() const
|
|||
// https://html.spec.whatwg.org/multipage/document-sequences.html#active-document
|
||||
DOM::Document const* BrowsingContext::active_document() const
|
||||
{
|
||||
// A browsing context's active document is its active window's associated Document.
|
||||
auto* window = active_window();
|
||||
if (!window)
|
||||
return nullptr;
|
||||
return &window->associated_document();
|
||||
// AD-HOC: The HTML Standard currently defines this as the active window's associated Document.
|
||||
// That changes too early when the initial about:blank Window is reused for its first
|
||||
// same-origin navigation, because create-and-initialize updates the associated Document
|
||||
// before the new Document is made active.
|
||||
// Spec issue: https://github.com/whatwg/html/issues/12415
|
||||
return m_active_document;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/document-sequences.html#active-document
|
||||
DOM::Document* BrowsingContext::active_document()
|
||||
{
|
||||
// A browsing context's active document is its active window's associated Document.
|
||||
auto* window = active_window();
|
||||
if (!window)
|
||||
return nullptr;
|
||||
return &window->associated_document();
|
||||
// AD-HOC: See the const overload above.
|
||||
return m_active_document;
|
||||
}
|
||||
|
||||
void BrowsingContext::set_active_document(GC::Ptr<DOM::Document> document)
|
||||
{
|
||||
m_active_document = document;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/browsers.html#active-window
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
|
||||
DOM::Document const* active_document() const;
|
||||
DOM::Document* active_document();
|
||||
void set_active_document(GC::Ptr<DOM::Document>);
|
||||
|
||||
HTML::WindowProxy* window_proxy();
|
||||
HTML::WindowProxy const* window_proxy() const;
|
||||
|
|
@ -93,6 +94,8 @@ private:
|
|||
// https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context
|
||||
GC::Ptr<WindowProxy> m_window_proxy;
|
||||
|
||||
GC::Ptr<DOM::Document> m_active_document;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/browsers.html#opener-browsing-context
|
||||
GC::Ptr<BrowsingContext> m_opener_browsing_context;
|
||||
|
||||
|
|
|
|||
|
|
@ -657,7 +657,10 @@ void ApplyHistoryStepState::start()
|
|||
signal_progress();
|
||||
continue;
|
||||
}
|
||||
queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), GC::create_function(heap(), [this, navigable] {
|
||||
// AD-HOC: Queue with navigable's active Document instead of using queue_global_task(active_window).
|
||||
// During initial about:blank Window reuse, active_window()->associated_document() can already be
|
||||
// the pending Document. This activation task must stay runnable against the current active Document.
|
||||
queue_a_task(Task::Source::NavigationAndTraversal, nullptr, navigable->active_document(), GC::create_function(heap(), [this, navigable] {
|
||||
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
|
||||
if (navigable->has_been_destroyed()) {
|
||||
++m_completed_change_jobs;
|
||||
|
|
@ -827,9 +830,12 @@ void ApplyHistoryStepState::start()
|
|||
*potentially_target_specific_source_snapshot_params, target_snapshot_params,
|
||||
user_involvement, {}, Navigable::NullOrError {},
|
||||
ContentSecurityPolicy::Directives::Directive::NavigationType::Other, allow_POST,
|
||||
GC::create_function(this->heap(), [this, after_document_populated](GC::Ptr<PopulateSessionHistoryEntryDocumentOutput> output) {
|
||||
GC::create_function(this->heap(), [this, after_document_populated, navigable](GC::Ptr<PopulateSessionHistoryEntryDocumentOutput> output) {
|
||||
VERIFY(m_traversable->active_window());
|
||||
queue_global_task(Task::Source::NavigationAndTraversal, *m_traversable->active_window(), GC::create_function(heap(), [after_document_populated, output]() {
|
||||
// AD-HOC: Queue with navigable's active Document instead of using queue_global_task(active_window).
|
||||
// During initial about:blank Window reuse, active_window()->associated_document() can already be
|
||||
// the pending Document. This continuation must stay runnable against the current active Document.
|
||||
queue_a_task(Task::Source::NavigationAndTraversal, nullptr, navigable->active_document(), GC::create_function(heap(), [after_document_populated, output]() {
|
||||
after_document_populated->function()(output);
|
||||
}));
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ Text/input/XHR/XMLHttpRequest-network-error-message.html
|
|||
Text/input/XHR/XMLHttpRequest-override-mimetype-blob.html
|
||||
Text/input/XML/error-page.html
|
||||
Text/input/base/a-element-target.html
|
||||
Text/input/navigation/iframe-initial-load-chunked-body.html
|
||||
Text/input/navigation/iframe-navigate-javascript-url.html
|
||||
Text/input/navigation/iframe-referrer-policy.html
|
||||
Text/input/parse-document-from-string-in-fetch-callback.html
|
||||
|
|
@ -230,6 +231,7 @@ Text/input/wpt-import/html/the-xhtml-syntax/parsing-xhtml-documents/xhtml-mathml
|
|||
Text/input/wpt-import/html/the-xhtml-syntax/parsing-xhtml-documents/xhtml-mathml-dtd-entity-9.htm
|
||||
Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/aborted-parser.window.html
|
||||
Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/url-entry-document-sync-call.window.html
|
||||
Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-entry-different-function-realm.html
|
||||
Text/input/wpt-import/navigation-api/navigation-methods/reload-state-and-info.html
|
||||
Text/input/wpt-import/navigation-api/navigation-methods/reload-state-undefined.html
|
||||
Text/input/wpt-import/page-visibility/test_child_document.html
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
chunked iframe loaded
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 5 tests
|
||||
|
||||
5 Pass
|
||||
Pass Fulfillment handler on fulfilled promise
|
||||
Pass Rejection handler on rejected promise
|
||||
Pass Fulfillment handler on pending-then-fulfilled promise
|
||||
Pass Rejection handler on pending-then-rejected promise
|
||||
Pass Thenable resolution
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
const childHTML = [
|
||||
"<!DOCTYPE html>",
|
||||
"<title>chunked iframe</title>",
|
||||
"<body>",
|
||||
Array.from({ length: 4096 }, (_, index) => `<span>${index}</span>`).join(""),
|
||||
"<p id=\"result\">chunked iframe loaded</p>",
|
||||
"</body>",
|
||||
].join("");
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/echo", false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify({
|
||||
method: "GET",
|
||||
path: "/echo/iframe-initial-load-chunked-body-child",
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
body: childHTML,
|
||||
}));
|
||||
</script>
|
||||
<iframe id="target" src="/echo/iframe-initial-load-chunked-body-child?chunks=1024,1024,1024,1024&chunk_delay_ms=1"></iframe>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
const iframe = document.getElementById("target");
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
println(iframe.contentDocument.getElementById("result").textContent);
|
||||
done();
|
||||
}, { once: true });
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Entry settings object for promise jobs when the function realm is different from the test realm</title>
|
||||
<script src="../../../../../resources/testharness.js"></script>
|
||||
<script src="../../../../../resources/testharnessreport.js"></script>
|
||||
<!-- https://github.com/whatwg/html/pull/5212 -->
|
||||
<!-- https://github.com/whatwg/html/issues/1426 -->
|
||||
|
||||
<!-- This is what would normally be considered the entry page. However, we use functions from the
|
||||
resources/function/function.html realm. So window.open() should resolve relative to that realm
|
||||
inside promise jobs. -->
|
||||
|
||||
<iframe src="resources/promise-job-entry-incumbent.html"></iframe>
|
||||
<iframe src="resources/function/function.html" id="function-frame"></iframe>
|
||||
|
||||
<script>
|
||||
setup({ explicit_done: true });
|
||||
|
||||
const relativeURL = "resources/window-to-open.html";
|
||||
const expectedURL = (new URL(relativeURL, document.querySelector("#function-frame").src)).href;
|
||||
|
||||
const incumbentWindow = frames[0];
|
||||
const functionWindow = frames[1];
|
||||
const FunctionFromAnotherWindow = frames[1].Function;
|
||||
|
||||
window.onload = () => {
|
||||
async_test(t => {
|
||||
const func = FunctionFromAnotherWindow(`
|
||||
const [incumbentWindow, relativeURL, t, assert_equals, expectedURL] = arguments[0];
|
||||
|
||||
const w = incumbentWindow.runWindowOpenVeryIndirectly(relativeURL);
|
||||
w.onload = t.step_func_done(() => {
|
||||
t.add_cleanup(() => w.close());
|
||||
assert_equals(w.location.href, expectedURL);
|
||||
});
|
||||
`);
|
||||
|
||||
Promise.resolve([incumbentWindow, relativeURL, t, assert_equals, expectedURL]).then(func);
|
||||
}, "Fulfillment handler on fulfilled promise");
|
||||
|
||||
async_test(t => {
|
||||
const func = FunctionFromAnotherWindow(`
|
||||
const [incumbentWindow, relativeURL, t, assert_equals, expectedURL] = arguments[0];
|
||||
|
||||
const w = incumbentWindow.runWindowOpenVeryIndirectly(relativeURL);
|
||||
w.onload = t.step_func_done(() => {
|
||||
t.add_cleanup(() => w.close());
|
||||
assert_equals(w.location.href, expectedURL);
|
||||
});
|
||||
`);
|
||||
|
||||
Promise.reject([incumbentWindow, relativeURL, t, assert_equals, expectedURL]).catch(func);
|
||||
}, "Rejection handler on rejected promise");
|
||||
|
||||
async_test(t => {
|
||||
let resolve;
|
||||
const p = new Promise(r => { resolve = r; });
|
||||
|
||||
const func = FunctionFromAnotherWindow(`
|
||||
const [incumbentWindow, relativeURL, t, assert_equals, expectedURL] = arguments[0];
|
||||
|
||||
const w = incumbentWindow.runWindowOpenVeryIndirectly(relativeURL);
|
||||
w.onload = t.step_func_done(() => {
|
||||
t.add_cleanup(() => w.close());
|
||||
assert_equals(w.location.href, expectedURL);
|
||||
});
|
||||
`);
|
||||
|
||||
p.then(func);
|
||||
t.step_timeout(() => resolve([incumbentWindow, relativeURL, t, assert_equals, expectedURL]), 0);
|
||||
}, "Fulfillment handler on pending-then-fulfilled promise");
|
||||
|
||||
async_test(t => {
|
||||
let reject;
|
||||
const p = new Promise((_, r) => { reject = r; });
|
||||
|
||||
const func = FunctionFromAnotherWindow(`
|
||||
const [incumbentWindow, relativeURL, t, assert_equals, expectedURL] = arguments[0];
|
||||
|
||||
const w = incumbentWindow.runWindowOpenVeryIndirectly(relativeURL);
|
||||
w.onload = t.step_func_done(() => {
|
||||
t.add_cleanup(() => w.close());
|
||||
assert_equals(w.location.href, expectedURL);
|
||||
});
|
||||
`);
|
||||
|
||||
p.catch(func);
|
||||
t.step_timeout(() => reject([incumbentWindow, relativeURL, t, assert_equals, expectedURL]), 0);
|
||||
}, "Rejection handler on pending-then-rejected promise");
|
||||
|
||||
async_test(t => {
|
||||
t.add_cleanup(() => { delete frames[1].args; });
|
||||
frames[1].args = [incumbentWindow, relativeURL, t, assert_equals, expectedURL];
|
||||
|
||||
const func = FunctionFromAnotherWindow(`
|
||||
const [incumbentWindow, relativeURL, t, assert_equals, expectedURL] = window.args;
|
||||
|
||||
const w = incumbentWindow.runWindowOpenVeryIndirectly(relativeURL);
|
||||
w.onload = t.step_func_done(() => {
|
||||
t.add_cleanup(() => w.close());
|
||||
assert_equals(w.location.href, expectedURL);
|
||||
});
|
||||
`);
|
||||
|
||||
const thenable = { then: func };
|
||||
|
||||
Promise.resolve(thenable);
|
||||
}, "Thenable resolution");
|
||||
|
||||
done();
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Current page used as a test helper</title>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>If the current settings object is used this page will be opened</title>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Realm for a "then" function used as a test helper</title>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>If the function's settings object is used this page will be opened</title>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Incumbent page used as a test helper</title>
|
||||
|
||||
<iframe src="relevant/relevant.html" id="r"></iframe>
|
||||
<iframe src="current/current.html" id="c"></iframe>
|
||||
|
||||
<script>
|
||||
const relevant = document.querySelector("#r");
|
||||
const current = document.querySelector("#c");
|
||||
|
||||
window.runWindowOpenVeryIndirectly = (...args) => {
|
||||
return current.contentWindow.open.call(relevant.contentWindow, ...args);
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Relevant page used as a test helper</title>
|
||||
|
||||
<script>
|
||||
// promise-job-incumbent will end up posting a message to here. We need to signal back the "source".
|
||||
|
||||
window.onmessage = e => {
|
||||
const testId = e.data;
|
||||
const sourceURL = e.source.document.URL;
|
||||
|
||||
window.dispatchEvent(new CustomEvent("messagereceived", { detail: [testId, sourceURL] }));
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>If the relevant settings object is used this page will be opened</title>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>If the entry settings object is used this page will be opened</title>
|
||||
Loading…
Reference in a new issue