diff --git a/moli-renderer-v8/src/dom_parser.rs b/moli-renderer-v8/src/dom_parser.rs index 88c712d8fb..5d6cee8427 100644 --- a/moli-renderer-v8/src/dom_parser.rs +++ b/moli-renderer-v8/src/dom_parser.rs @@ -242,28 +242,7 @@ pub(crate) fn install_dom_parser_template_bindings<'s>( ); } -pub(super) fn parse_detached_document_from_string<'s>( - scope: &mut v8::PinScope<'s, '_>, - source: &str, - mime: &str, -) -> Option> { - let is_html = is_html_document_mime(mime); - let is_xml = is_dom_parser_xml_mime(mime); - if !is_html && !is_xml { - return None; - } - - let host_ptr = context_host_ptr_from_global_bridge(scope)?; - let runtime = unsafe { &*host_ptr }; - parse_detached_document_from_string_with_url( - scope, - runtime.document_url().clone(), - source, - mime, - ) -} - -fn parse_detached_document_from_string_with_url<'s>( +pub(super) fn parse_detached_document_from_string_with_url<'s>( scope: &mut v8::PinScope<'s, '_>, document_url: Url, source: &str, diff --git a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs index 3a085bff3a..98021e87b1 100644 --- a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs +++ b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs @@ -223,8 +223,13 @@ fn apply_xhr_response_body( XmlHttpRequestResponseType::Document => { let mime = xhr_response_mime_essence(scope, xhr, &head.headers) .unwrap_or_else(|| "text/html".to_owned()); - let document = - parse_xhr_response_document(scope, body_text.as_deref().unwrap_or(""), Some(&mime)); + let document = parse_xhr_response_document( + scope, + xhr, + &head, + body_text.as_deref().unwrap_or(""), + Some(&mime), + ); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); document } @@ -242,7 +247,7 @@ fn apply_xhr_response_body( let document = parse_default_xhr_response_xml( scope, xhr, - &head.headers, + &head, body_text.as_deref().unwrap_or(""), ); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); @@ -283,14 +288,13 @@ fn apply_xhr_response_text( XmlHttpRequestResponseType::Document => { let mime = xhr_response_mime_essence(scope, xhr, &head.headers) .unwrap_or_else(|| "text/html".to_owned()); - let document = parse_xhr_response_document(scope, &body_text, Some(&mime)); + let document = parse_xhr_response_document(scope, xhr, &head, &body_text, Some(&mime)); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); document } XmlHttpRequestResponseType::Default | XmlHttpRequestResponseType::Text => { if response_type == XmlHttpRequestResponseType::Default { - let document = - parse_default_xhr_response_xml(scope, xhr, &head.headers, &body_text); + let document = parse_default_xhr_response_xml(scope, xhr, &head, &body_text); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); } v8_string(scope, &body_text) @@ -416,11 +420,7 @@ fn set_xhr_response_head( XHR_STATUS_TEXT_SLOT, status_text.unwrap_or_else(|| http_status_text(head.status)), ); - let response_url = head - .redirect_chain - .last() - .map(|redirect| &redirect.to_url) - .unwrap_or(&head.final_url); + let response_url = xhr_response_url(head); set_xhr_state_string(scope, xhr, XHR_RESPONSE_URL_SLOT, response_url.as_str()); // All delivery paths expose the same filtered header list, including @@ -593,15 +593,24 @@ fn xhr_response_mime_essence( fn parse_default_xhr_response_xml<'s>( scope: &mut v8::PinScope<'s, '_>, xhr: v8::Local<'_, v8::Object>, - headers: &[(String, String)], + head: &moli_fetch::ResponseHead, body_text: &str, ) -> v8::Local<'s, v8::Value> { - let mime = xhr_response_mime_essence(scope, xhr, headers); - parse_xhr_response_document(scope, body_text, mime.as_deref()) + let mime = xhr_response_mime_essence(scope, xhr, &head.headers); + parse_xhr_response_document(scope, xhr, head, body_text, mime.as_deref()) +} + +fn xhr_response_url(head: &moli_fetch::ResponseHead) -> &url::Url { + head.redirect_chain + .last() + .map(|redirect| &redirect.to_url) + .unwrap_or(&head.final_url) } fn parse_xhr_response_document<'s>( scope: &mut v8::PinScope<'s, '_>, + xhr: v8::Local<'_, v8::Object>, + head: &moli_fetch::ResponseHead, body_text: &str, mime: Option<&str>, ) -> v8::Local<'s, v8::Value> { @@ -610,11 +619,55 @@ fn parse_xhr_response_document<'s>( else { return v8::null(scope).into(); }; - dom_parser::parse_detached_document_from_string(scope, body_text, mime) - .map(|value| value.into()) + build_xhr_response_document(scope, xhr, xhr_response_url(head).clone(), body_text, mime) + .map(Into::into) .unwrap_or_else(|| v8::null(scope).into()) } +fn build_xhr_response_document<'s>( + scope: &mut v8::PinScope<'s, '_>, + xhr: v8::Local<'_, v8::Object>, + response_url: url::Url, + body_text: &str, + mime: &str, +) -> Option> { + let xhr = v8::Global::new(scope, xhr); + let xhr = v8::Local::new(scope, xhr); + let context = xhr.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); + let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host = unsafe { &*host_ptr }; + let binding = xhr_execution_context_binding(scope, host, xhr)?; + let origin_document_handle = match binding.dispatch_scope() { + crate::native_bridge::OwnerDispatchScope::Top => host.document_handle(), + crate::native_bridge::OwnerDispatchScope::Child(handle) => { + host.child_browsing_context_document_handle(handle)? + } + crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => { + host.lightweight_popup_document_handle(popup_id)? + } + }; + let origin_document = unsafe { &mut *host_ptr }.native_bridge_mut().wrap_handle( + scope, + host_ptr, + origin_document_handle, + )?; + let document = dom_parser::parse_detached_document_from_string_with_url( + scope, + response_url, + body_text, + mime, + )?; + // The response URL controls relative URL resolution; its origin does not + // replace the XHR environment's origin (including for CORS responses). + crate::native_bridge::document::inherit_detached_document_origin( + scope, + document, + origin_document, + ); + Some(document) +} + fn xhr_response_blob_mime_type( scope: &mut v8::PinScope<'_, '_>, xhr: v8::Local<'_, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index da5a7c3303..4c4db32035 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -1079,6 +1079,91 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( assert_eq!(result, "4|true|html|true|false|true"); } +#[test] +fn xml_http_request_response_document_uses_response_url_and_requester_origin() { + for child_realm in [false, true] { + for (mime, response_type) in [ + ("application/xml", ""), + ("application/xml", "document"), + ("text/html", "document"), + ] { + let mut vm = new_parsed_test_vm( + "https://requester.example/page/index.html", + "", + ); + vm.eval(&format!( + r#"(() => {{ + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + globalThis.__originRealm = {child_realm} ? frame.contentWindow : self; + globalThis.__documentXhr = new __originRealm.XMLHttpRequest(); + __documentXhr.open('GET', '/initial'); + __documentXhr.responseType = '{response_type}'; + }})()"# + )) + .expect("create XHR in its owning realm"); + let context_ptr: *const v8::Global = &vm.page_default_context; + vm.renderer_document_isolate.with_entered_renderer_document_isolate(move |isolate| { + let scope = std::pin::pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + let context = unsafe { v8::Local::new(scope, &*context_ptr) }; + let scope = &mut v8::ContextScope::new(scope, context); + let xhr = context.global(scope).get(scope, v8str(scope, "__documentXhr").into()) + .and_then(|value| v8::Local::::try_from(value).ok()).unwrap(); + crate::network_host::apply_xhr_response_body_source( + scope, + xhr, + moli_fetch::ResponseHead { + final_url: Url::parse("https://response.example/resource/doc#fragment").unwrap(), + status: 200, + headers: vec![("Content-Type".to_owned(), mime.to_owned())], + request_cookie_report: None, + cookie_set_reports: Vec::new(), + redirected: false, + redirect_chain: Vec::new(), + from_cache: false, + negotiated_http_version: None, + }, + moli_fetch::ResponseBody::materialized_bytes(br#"link"#.to_vec()), + ); + Ok(()) + }).expect("deliver response from a different origin in the parent realm"); + let result = vm + .eval( + r#"(() => { + const doc = __documentXhr.responseXML; + const check = (value, message) => { if (!value) throw new Error(message); }; + const url = 'https://response.example/resource/doc#fragment'; + check(doc instanceof __originRealm.Document, 'XHR creation realm'); + check(doc.URL === url && doc.documentURI === url, 'response URL metadata'); + check(__documentXhr.responseURL === url.split('#')[0], 'responseURL serialization'); + check(doc.domain === 'requester.example', 'origin belongs to requester'); + check(doc.defaultView === null && doc.hidden, 'windowless response document'); + check(doc.baseURI === 'https://response.example/assets/', 'relative parsed base'); + const link = doc.getElementById('link'); + check(link.href === 'https://response.example/assets/child', 'relative link'); + doc.querySelector('base').remove(); + check(doc.baseURI === url && link.baseURI === url, 'base removal'); + const base = doc.createElementNS('http://www.w3.org/1999/xhtml', 'base'); + base.href = '../changed/'; + doc.documentElement.appendChild(base); + check(doc.baseURI === 'https://response.example/changed/', 'base insertion'); + document.head.appendChild(base); + check(doc.baseURI === url, 'base adoption'); + __documentXhr.open('GET', '/next'); + check(__documentXhr.responseXML === null && doc.URL === url, 'XHR reuse'); + return 'ok'; + })()"#, + ) + .expect("response document metadata should remain coherent"); + assert_eq!( + result, "ok", + "child_realm={child_realm}, mime={mime}, response_type={response_type}" + ); + } + } +} + #[test] fn xml_http_request_serializes_document_bodies_and_limits_charset_rewriting_to_text() { let vm = new_storage_test_vm("https://xhr-document-body.test/");