diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index cb37e42c59..73737cd3ec 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3631,7 +3631,6 @@ svg/types/scripted/SVGLengthList-basics.html svg/types/scripted/SVGList-parse-invalid-clears-items.html svg/types/scripted/SVGMatrix-tentative.html svg/types/scripted/SVGPoint.html -trusted-types/Document-write-appending-line-feed.html trusted-types/Document-write.html trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-TT-realm.html trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-non-TT-realm.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index f4e7927d92..1891efa00c 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -7957,6 +7957,7 @@ trusted-types/DedicatedWorker-eval.html trusted-types/DedicatedWorker-importScripts.html trusted-types/DedicatedWorker-setTimeout-setInterval.html trusted-types/Document-execCommand.html +trusted-types/Document-write-appending-line-feed.html trusted-types/Element-insertAdjacentHTML.html trusted-types/Element-outerHTML.html trusted-types/Element-setAttribute.html diff --git a/moli-renderer-v8/src/native_bridge/document/lifecycle.rs b/moli-renderer-v8/src/native_bridge/document/lifecycle.rs index 519c18893f..c8c98ae776 100644 --- a/moli-renderer-v8/src/native_bridge/document/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/document/lifecycle.rs @@ -20,17 +20,18 @@ use crate::{ dom::native::{NativeDom, NodeData}, parser::HtmlParser, util::{ - call_object_method, node_wrapper_from_handle, utf16_next_scalar_boundary, - utf16_previous_scalar_boundary, utf16_replace_units_range_lossy, utf16_units, v8str, + call_object_method, get_private_value, node_wrapper_from_handle, set_private_value, + utf16_next_scalar_boundary, utf16_previous_scalar_boundary, utf16_replace_units_range_lossy, + utf16_units, v8_string, v8str, }, webidl, }; -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.write")] -struct DocumentWriteArgs { - #[webidl(variadic)] - text: Vec, +const DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT: &str = "__moliDetachedDocumentWriteStreamOpen"; + +struct DocumentWriteInput { + text: String, + is_trusted: bool, } pub(in crate::native_bridge) fn node_document_write_callback<'s>( @@ -64,9 +65,44 @@ fn node_document_write_or_writeln_callback<'s>( rv.set_undefined(); return; } - let Some(parsed) = webidl::parse_args::(scope, &args) else { - return; + let api_prefix = if append_newline { + "Document.writeln" + } else { + "Document.write" }; + let input = match document_write_input(scope, &args, api_prefix) { + Ok(input) => input, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + let sink = if append_newline { + "Document writeln" + } else { + "Document write" + }; + let api_name = if append_newline { "writeln" } else { "write" }; + let mut html = input.text; + if !input.is_trusted { + let Some(value) = v8_string(scope, &html) else { + return; + }; + let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope); + let Some(compliant) = crate::context_bootstrap::trusted_html_string_or_throw( + scope, + value.into(), + requirements, + sink, + api_name, + ) else { + return; + }; + html = compliant; + } + if append_newline { + html.push('\n'); + } if !is_html_document(unsafe { &*runtime_ptr }, handle) { throw_dom_exception( scope, @@ -86,11 +122,19 @@ fn node_document_write_or_writeln_callback<'s>( return; } if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() { - let mut html = parsed.text.concat(); - if append_newline { - html.push('\n'); + let document = args.this(); + let stream_was_open = detached_document_write_stream_is_open(scope, document); + if !stream_was_open { + set_detached_document_write_stream_open(scope, document, true); + } + let wrote = if stream_was_open { + append_detached_html_document_body_html(scope, runtime_ptr, handle, &html) + } else { + set_detached_html_document_body_html(scope, runtime_ptr, handle, &html) + }; + if !wrote && !stream_was_open { + set_detached_document_write_stream_open(scope, document, false); } - append_detached_html_document_body_html(scope, runtime_ptr, handle, &html); rv.set_undefined(); return; } @@ -107,15 +151,34 @@ fn node_document_write_or_writeln_callback<'s>( clear_window_event_handlers(scope); runtime.prepare_root_document_replacement(scope, runtime_ptr, handle); } - for chunk in parsed.text { - let _ = runtime.write_html(scope, runtime_ptr, handle, &chunk); - } - if append_newline { - let _ = runtime.write_html(scope, runtime_ptr, handle, "\n"); - } + let _ = runtime.write_html(scope, runtime_ptr, handle, &html); rv.set_undefined(); } +fn document_write_input<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + api_prefix: &'static str, +) -> Result { + let mut text = String::new(); + let mut is_trusted = true; + for index in 0..args.length() { + let value = args.get(index); + if let Some(value) = crate::context_bootstrap::trusted_html_value_string(scope, value) { + text.push_str(&value); + continue; + } + is_trusted = false; + let value = webidl::convert::( + scope, + value, + webidl::Context::argument(api_prefix, (index + 1) as usize), + )?; + text.push_str(&value.0); + } + Ok(DocumentWriteInput { text, is_trusted }) +} + fn current_script_ignores_document_write_without_parser_insertion_point( runtime: &JsContextHost, ) -> bool { @@ -181,7 +244,11 @@ pub(in crate::native_bridge) fn node_document_open_callback<'s>( return; } if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() { - set_detached_html_document_body_html(scope, runtime_ptr, handle, ""); + let document = args.this(); + set_detached_document_write_stream_open(scope, document, true); + if !set_detached_html_document_body_html(scope, runtime_ptr, handle, "") { + set_detached_document_write_stream_open(scope, document, false); + } rv.set(args.this().into()); return; } @@ -300,6 +367,7 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>( return; } if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() { + set_detached_document_write_stream_open(scope, args.this(), false); rv.set_undefined(); return; } @@ -308,6 +376,27 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>( rv.set_undefined(); } +fn detached_document_write_stream_is_open<'s>( + scope: &mut v8::PinScope<'s, '_>, + document: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, document, DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT) + .is_some_and(|value| value.boolean_value(scope)) +} + +fn set_detached_document_write_stream_open( + scope: &mut v8::PinScope<'_, '_>, + document: v8::Local<'_, v8::Object>, + open: bool, +) { + set_private_value( + scope, + document, + DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT, + v8::Boolean::new(scope, open).into(), + ); +} + fn detached_html_document_body_handle( runtime: &JsContextHost, document_handle: DomHandle, @@ -329,6 +418,9 @@ pub(in crate::native_bridge) fn set_detached_html_document_body_html( let Some(body) = detached_html_document_body_handle(runtime, document_handle) else { return false; }; + if html.is_empty() && runtime.dom_host().child_handles(body).next().is_none() { + return true; + } runtime.set_inner_html(scope, runtime_ptr, body, html) }) } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 1820fffc89..cedeb86679 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -434,6 +434,86 @@ fn document_parse_html_unsafe_gates_converted_union_source() { ); } +#[test] +fn document_write_gates_concatenated_union_values_before_writeln_newline() { + let mut vm = new_storage_test_vm("https://document-write-trusted-types.test/"); + vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); + + let result = vm + .eval( + r#" +(() => { + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error && error.name; + } + }; + const custom = trustedTypes.createPolicy("document-write-custom", { + createHTML: value => `(${value})` + }); + const doc = new DOMParser().parseFromString("", "text/html"); + const replacementDoc = new DOMParser().parseFromString( + custom.createHTML("seed"), + "text/html" + ); + const reset = () => { doc.body.innerHTML = trustedTypes.emptyHTML; }; + const blocked = errorName(() => doc.write("blocked")); + + doc.write(custom.createHTML("1"), custom.createHTML("2")); + const allTrusted = doc.body.innerHTML; + replacementDoc.write(custom.createHTML("replacement")); + replacementDoc.writeln(custom.createHTML("tail")); + const replacesDetachedContent = replacementDoc.body.innerHTML; + reset(); + + const defaultCalls = []; + trustedTypes.createPolicy("default", { + createHTML: (value, type, sink) => { + defaultCalls.push([value, type, sink]); + return `[${value}]`; + } + }); + + doc.write("1", "2"); + const strings = doc.body.innerHTML; + reset(); + doc.write(custom.createHTML("1"), "2"); + const mixed = doc.body.innerHTML; + reset(); + doc.writeln("3", "4"); + const stringLine = doc.body.innerHTML; + reset(); + doc.writeln(custom.createHTML("3"), custom.createHTML("4")); + const trustedLine = doc.body.innerHTML; + reset(); + doc.writeln(); + const emptyLine = doc.body.innerHTML; + + return JSON.stringify({ + blocked, + allTrusted, + replacesDetachedContent, + strings, + mixed, + stringLine, + trustedLine, + emptyLine, + defaultCalls + }); +})() +"#, + ) + .expect("Document.write TrustedHTML union probe should evaluate"); + + assert_eq!( + result, + r#"{"blocked":"TypeError","allTrusted":"(1)(2)","replacesDetachedContent":"(replacement)(tail)\n","strings":"[12]","mixed":"[(1)2]","stringLine":"[34]\n","trustedLine":"(3)(4)\n","emptyLine":"\n","defaultCalls":[["12","TrustedHTML","Document write"],["(1)2","TrustedHTML","Document write"],["34","TrustedHTML","Document writeln"]]}"# + ); +} + #[test] fn script_elements_preserve_only_parser_or_trusted_script_source() { let mut vm = new_storage_test_vm("https://script-source-trusted-types.test/"); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs index 86e6ed91dd..5782bff191 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs @@ -99,6 +99,7 @@ fn detached_document_write_preserves_existing_noscript_text() { r#" (() => { const doc = document.implementation.createHTMLDocument(""); + doc.open(); const noscript = doc.createElement("noscript"); noscript.textContent = "fallback&"; doc.body.append(noscript);