mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 08:00:56 +00:00
fix(renderer): execute javascript URL navigations internally
This commit is contained in:
@@ -22,6 +22,7 @@ mod history_mutation;
|
||||
mod history_runtime;
|
||||
mod idle_detection;
|
||||
mod image_data;
|
||||
mod javascript_url;
|
||||
pub(crate) use self::idle_detection::apply_idle_override_to_current_context;
|
||||
mod indexed_db;
|
||||
#[cfg(test)]
|
||||
@@ -285,6 +286,10 @@ pub(crate) use self::indexed_db::{
|
||||
pub(in crate::context_bootstrap) use self::indexed_db::{
|
||||
indexed_db_usage_bytes_for_storage_key, scoped_storage_bucket_indexed_db_factory,
|
||||
};
|
||||
pub(crate) use self::javascript_url::{
|
||||
arm_internal_javascript_url_eval, consume_internal_javascript_url_eval,
|
||||
restore_internal_javascript_url_eval,
|
||||
};
|
||||
pub(crate) use self::location_runtime::sync_global_location_runtime_state;
|
||||
pub(crate) use self::location_runtime::{
|
||||
sync_document_location_runtime_state_from_window,
|
||||
@@ -422,8 +427,9 @@ pub(crate) use self::streams::{
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::trusted_types::trusted_types_lazy_state_materialized;
|
||||
pub(crate) use self::trusted_types::{
|
||||
TrustedTypesCodeGenerationCheck, install_trusted_types_eval_runtime_state,
|
||||
install_trusted_types_runtime_state, trusted_html_string_or_throw, trusted_html_value_string,
|
||||
TrustedTypesCodeGenerationCheck, check_javascript_url_trusted_types,
|
||||
install_trusted_types_eval_runtime_state, install_trusted_types_runtime_state,
|
||||
trusted_html_string_or_throw, trusted_html_value_string,
|
||||
trusted_script_string_for_script_element_execution, trusted_script_string_or_type_error,
|
||||
trusted_script_url_string_or_throw, trusted_types_code_generation_check,
|
||||
trusted_types_code_generation_check_callback,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::util::{get_private_value, set_private_value};
|
||||
|
||||
const INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT: &str = "__moliInternalJavascriptUrlEvalPermit";
|
||||
|
||||
/// Scoped permission for the direct eval used by the lightweight-popup
|
||||
/// javascript-URL adapter.
|
||||
///
|
||||
/// Lightweight popups do not own a V8 Context, so their adapter uses direct
|
||||
/// eval to recover Script completion semantics while resolving globals
|
||||
/// through the popup Window object. The source has already passed the target
|
||||
/// Document's CSP and Trusted Types checks. The isolate callback consumes the
|
||||
/// private marker on the first compilation; nested page-authored eval calls
|
||||
/// therefore go through the normal policy path.
|
||||
pub(crate) struct InternalJavascriptUrlEvalPermit {
|
||||
previous: Option<v8::Global<v8::Value>>,
|
||||
}
|
||||
|
||||
pub(crate) fn arm_internal_javascript_url_eval(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> InternalJavascriptUrlEvalPermit {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let previous = get_private_value(scope, global, INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT)
|
||||
.map(|value| v8::Global::new(scope, value));
|
||||
let marker = v8::Object::new(scope);
|
||||
set_private_value(
|
||||
scope,
|
||||
global,
|
||||
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
|
||||
marker.into(),
|
||||
);
|
||||
InternalJavascriptUrlEvalPermit { previous }
|
||||
}
|
||||
|
||||
pub(crate) fn restore_internal_javascript_url_eval(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
permit: InternalJavascriptUrlEvalPermit,
|
||||
) {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let previous = permit
|
||||
.previous
|
||||
.as_ref()
|
||||
.map(|value| v8::Local::new(scope, value))
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
set_private_value(
|
||||
scope,
|
||||
global,
|
||||
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
|
||||
previous,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn consume_internal_javascript_url_eval(scope: &mut v8::PinScope<'_, '_>) -> bool {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let armed = get_private_value(scope, global, INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT)
|
||||
.is_some_and(|marker| marker.is_object());
|
||||
if !armed {
|
||||
return false;
|
||||
}
|
||||
let empty = v8::undefined(scope);
|
||||
set_private_value(
|
||||
scope,
|
||||
global,
|
||||
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
|
||||
empty.into(),
|
||||
);
|
||||
true
|
||||
}
|
||||
@@ -265,6 +265,59 @@ pub(crate) fn trusted_script_string_for_script_element_execution(
|
||||
None
|
||||
}
|
||||
|
||||
/// Result of applying the Trusted Types pre-navigation conversion to a
|
||||
/// decoded `javascript:` source.
|
||||
///
|
||||
/// Owner-specific CSP event dispatch deliberately remains outside this
|
||||
/// module. This layer owns only Trusted Type values and default-policy calls.
|
||||
pub(crate) struct JavascriptUrlTrustedTypesCheck {
|
||||
pub(crate) source: Option<String>,
|
||||
pub(crate) violated: bool,
|
||||
}
|
||||
|
||||
/// Apply the Trusted Types pre-navigation conversion for a decoded
|
||||
/// `javascript:` source. Policy exceptions are consumed because this runs in
|
||||
/// the later navigation task, not in the API call that queued the navigation.
|
||||
pub(crate) fn check_javascript_url_trusted_types(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
original: &str,
|
||||
requirements: TrustedTypesForScriptRequirements,
|
||||
) -> JavascriptUrlTrustedTypesCheck {
|
||||
if !requirements.requires_conversion() {
|
||||
return JavascriptUrlTrustedTypesCheck {
|
||||
source: Some(original.to_owned()),
|
||||
violated: false,
|
||||
};
|
||||
}
|
||||
let outcome = {
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let mut scope = try_catch.init();
|
||||
apply_default_trusted_type_policy_outcome(
|
||||
&mut scope,
|
||||
original,
|
||||
TrustedTypeKind::Script,
|
||||
"Location href",
|
||||
)
|
||||
};
|
||||
match outcome {
|
||||
DefaultTrustedTypePolicyOutcome::Value(value)
|
||||
if url::Url::parse(&format!("javascript:{value}")).is_ok() =>
|
||||
{
|
||||
JavascriptUrlTrustedTypesCheck {
|
||||
source: Some(value),
|
||||
violated: false,
|
||||
}
|
||||
}
|
||||
DefaultTrustedTypePolicyOutcome::Value(_)
|
||||
| DefaultTrustedTypePolicyOutcome::Unavailable
|
||||
| DefaultTrustedTypePolicyOutcome::Rejected
|
||||
| DefaultTrustedTypePolicyOutcome::Exception => JavascriptUrlTrustedTypesCheck {
|
||||
source: (!requirements.is_enforced()).then(|| original.to_owned()),
|
||||
violated: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum TrustedTypesCodeGenerationCheck {
|
||||
AllowOriginal,
|
||||
AllowModified(String),
|
||||
|
||||
@@ -777,7 +777,7 @@ impl DocumentRuntime {
|
||||
kind: ContentSecurityPolicyNonUrlKind,
|
||||
source: &str,
|
||||
) -> DocumentContentSecurityPolicyCheck {
|
||||
self.inline_source_csp_check_for_child_document(
|
||||
self.inline_source_csp_check_for_document(
|
||||
Some(self.document_handle()),
|
||||
self.document_url(),
|
||||
&self.policy_container.response_content_security_policies,
|
||||
@@ -795,7 +795,7 @@ impl DocumentRuntime {
|
||||
source: &str,
|
||||
request: ContentSecurityPolicyScriptElementRequest<'_>,
|
||||
) -> DocumentContentSecurityPolicyCheck {
|
||||
self.inline_script_element_csp_check_for_child_document(
|
||||
self.inline_script_element_csp_check_for_document(
|
||||
Some(self.document_handle()),
|
||||
self.document_url(),
|
||||
&self.policy_container.response_content_security_policies,
|
||||
@@ -929,7 +929,7 @@ impl DocumentRuntime {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn inline_script_element_csp_check_for_child_document(
|
||||
pub(crate) fn inline_script_element_csp_check_for_document(
|
||||
&self,
|
||||
document_handle: Option<DomHandle>,
|
||||
document_url: &Url,
|
||||
@@ -971,7 +971,7 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inline_source_csp_check_for_child_document(
|
||||
pub(crate) fn inline_source_csp_check_for_document(
|
||||
&self,
|
||||
document_handle: Option<DomHandle>,
|
||||
document_url: &Url,
|
||||
@@ -1011,6 +1011,7 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn document_frame_csp_violation(
|
||||
&self,
|
||||
request_url: &Url,
|
||||
@@ -1030,22 +1031,6 @@ impl DocumentRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn document_frame_csp_report_only_violation(
|
||||
&self,
|
||||
request_url: &Url,
|
||||
) -> Option<DocumentContentSecurityPolicyViolation> {
|
||||
document_frame_policy_violation(
|
||||
&self
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&self.policy_container.content_security_reporting_endpoints,
|
||||
self.document_url(),
|
||||
request_url,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn script_element_request_csp_violation_for_child_document(
|
||||
&self,
|
||||
document_handle: Option<DomHandle>,
|
||||
@@ -1164,7 +1149,7 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn document_frame_csp_violation_for_child_document(
|
||||
pub(crate) fn document_frame_csp_violation_for_document(
|
||||
&self,
|
||||
document_handle: Option<DomHandle>,
|
||||
document_url: &Url,
|
||||
@@ -1186,7 +1171,7 @@ impl DocumentRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn document_frame_csp_report_only_violation_for_child_document(
|
||||
pub(crate) fn document_frame_csp_report_only_violation_for_document(
|
||||
&self,
|
||||
document_url: &Url,
|
||||
response_report_only_policies: &[String],
|
||||
@@ -1396,6 +1381,44 @@ impl DocumentRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn trusted_types_sink_csp_violations_for_document(
|
||||
&self,
|
||||
document_handle: Option<DomHandle>,
|
||||
document_url: &Url,
|
||||
response_policies: &[String],
|
||||
report_only_policies: &[String],
|
||||
reporting_endpoints: &ContentSecurityPolicyReportingEndpoints,
|
||||
sink: &str,
|
||||
sample: &str,
|
||||
) -> Vec<DocumentContentSecurityPolicyViolation> {
|
||||
let report_only_policies = document_response_content_security_policy_strings(
|
||||
report_only_policies,
|
||||
reporting_endpoints,
|
||||
);
|
||||
let enforced_policies = self
|
||||
.document_content_security_policy_strings_for_optional_document(
|
||||
document_handle,
|
||||
response_policies,
|
||||
reporting_endpoints,
|
||||
);
|
||||
let mut violations = iter_document_trusted_types_sink_policy_violations(
|
||||
enforced_policies,
|
||||
document_url,
|
||||
sink,
|
||||
sample,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
violations.extend(iter_document_trusted_types_sink_policy_violations(
|
||||
report_only_policies,
|
||||
document_url,
|
||||
sink,
|
||||
sample,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
));
|
||||
violations
|
||||
}
|
||||
|
||||
pub(crate) fn requires_trusted_types_for_script(&self) -> bool {
|
||||
let policies = self.document_content_security_policy_strings_for_optional_document(
|
||||
Some(self.document_handle()),
|
||||
@@ -2131,7 +2154,24 @@ fn document_trusted_types_sink_policy_violation_from_document_policies(
|
||||
sample: &str,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> Option<DocumentContentSecurityPolicyViolation> {
|
||||
policies.into_iter().find_map(|policy| {
|
||||
iter_document_trusted_types_sink_policy_violations(
|
||||
policies,
|
||||
document_url,
|
||||
sink,
|
||||
sample,
|
||||
disposition,
|
||||
)
|
||||
.next()
|
||||
}
|
||||
|
||||
fn iter_document_trusted_types_sink_policy_violations<'a>(
|
||||
policies: Vec<DocumentContentSecurityPolicyString>,
|
||||
document_url: &'a Url,
|
||||
sink: &'a str,
|
||||
sample: &'a str,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> impl Iterator<Item = DocumentContentSecurityPolicyViolation> + 'a {
|
||||
policies.into_iter().filter_map(move |policy| {
|
||||
let single_policy = [policy.policy.clone()];
|
||||
let mut violation =
|
||||
content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints(
|
||||
|
||||
@@ -200,6 +200,21 @@ enum LightweightPopupClassicScriptAdvance {
|
||||
Pending(PopupDocumentLoadBodyActivity),
|
||||
}
|
||||
|
||||
struct LightweightPopupDocumentProjectionSource<'a> {
|
||||
task: LightweightPopupNavigationTaskToken,
|
||||
document_url: &'a Url,
|
||||
markup: &'a str,
|
||||
content_type: Option<&'a str>,
|
||||
character_set: Option<&'a str>,
|
||||
policy_container: &'a DocumentPolicyContainer,
|
||||
}
|
||||
|
||||
struct LightweightPopupDocumentProjectionApplication {
|
||||
document_handle: Option<DomHandle>,
|
||||
body_activity: PopupDocumentLoadBodyActivity,
|
||||
classic_script_load_pending: bool,
|
||||
}
|
||||
|
||||
pub(super) struct PendingLightweightPopupClassicScriptLoad {
|
||||
target: LightweightPopupClassicScriptFetchTarget,
|
||||
script_handle: DomHandle,
|
||||
@@ -2408,81 +2423,25 @@ impl JsContextHost {
|
||||
&base_url,
|
||||
&document_referrer,
|
||||
);
|
||||
if let Some(document) =
|
||||
crate::dom_parser::parse_child_document_projection_from_source(
|
||||
scope,
|
||||
final_url.clone(),
|
||||
&loaded.markup,
|
||||
loaded.content_type.as_deref(),
|
||||
Some(&loaded.character_set),
|
||||
crate::parser::HtmlParser::with_scripting_enabled(
|
||||
self.lightweight_popup_scripting_enabled(popup_id),
|
||||
),
|
||||
)
|
||||
{
|
||||
let host_ptr = self as *mut JsContextHost;
|
||||
let document_base_url = lightweight_popup_parsed_document_base_url(
|
||||
host_ptr, scope, document, &final_url,
|
||||
);
|
||||
if let Some(document_record) =
|
||||
self.lightweight_popup_document_record_mut(popup_id)
|
||||
{
|
||||
document_record.state.base_url = document_base_url.clone();
|
||||
}
|
||||
let document_referrer = self
|
||||
.lightweight_popup_document_record(popup_id)
|
||||
.map(|document| document.state.policy_container.document_referrer.clone())
|
||||
.unwrap_or_default();
|
||||
sync_lightweight_popup_document_window_slots(
|
||||
scope,
|
||||
document,
|
||||
window,
|
||||
&document_base_url,
|
||||
&document_referrer,
|
||||
);
|
||||
set_object_slot(scope, window, "document", document.into());
|
||||
self.forget_lightweight_popup_document_handle(popup_id);
|
||||
let popup_document_handle =
|
||||
self.remember_lightweight_popup_document_handle(scope, popup_id, document);
|
||||
if let Some(document_handle) = popup_document_handle {
|
||||
let _ = crate::context_bootstrap::install_css_runtime_state_for_document(
|
||||
scope,
|
||||
window,
|
||||
Some(document_handle),
|
||||
);
|
||||
install_lightweight_popup_get_computed_style(
|
||||
scope,
|
||||
window,
|
||||
document_handle,
|
||||
);
|
||||
}
|
||||
let _ = self.set_lightweight_popup_document_wrapper(
|
||||
popup_id,
|
||||
v8::Global::new(scope, document),
|
||||
);
|
||||
let script_advance = self.execute_lightweight_popup_document_scripts(
|
||||
scope,
|
||||
if let Some(application) = self.install_lightweight_popup_document_projection(
|
||||
scope,
|
||||
window,
|
||||
LightweightPopupDocumentProjectionSource {
|
||||
task,
|
||||
popup_document_handle,
|
||||
loaded.policy_container.sandbox.allows_scripts,
|
||||
&loaded.policy_container.response_content_security_policies,
|
||||
&loaded
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&loaded.policy_container.content_security_reporting_endpoints,
|
||||
);
|
||||
body_activity = match script_advance {
|
||||
LightweightPopupClassicScriptAdvance::Completed(activity) => activity,
|
||||
LightweightPopupClassicScriptAdvance::Pending(activity) => {
|
||||
classic_script_load_pending = true;
|
||||
activity
|
||||
}
|
||||
};
|
||||
document_url: &final_url,
|
||||
markup: &loaded.markup,
|
||||
content_type: loaded.content_type.as_deref(),
|
||||
character_set: Some(&loaded.character_set),
|
||||
policy_container: &loaded.policy_container,
|
||||
},
|
||||
) {
|
||||
body_activity = application.body_activity;
|
||||
classic_script_load_pending = application.classic_script_load_pending;
|
||||
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
|
||||
return PopupDocumentLoadApplication::Applied { body_activity };
|
||||
}
|
||||
if !classic_script_load_pending
|
||||
&& let Some(document_handle) = popup_document_handle
|
||||
&& let Some(document_handle) = application.document_handle
|
||||
{
|
||||
self.sync_child_browsing_context_subtree(scope, document_handle);
|
||||
}
|
||||
@@ -2801,6 +2760,83 @@ impl JsContextHost {
|
||||
self.dispatch_lightweight_popup_window_event(scope, popup_id, "load", event);
|
||||
}
|
||||
|
||||
fn install_lightweight_popup_document_projection<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
source: LightweightPopupDocumentProjectionSource<'_>,
|
||||
) -> Option<LightweightPopupDocumentProjectionApplication> {
|
||||
let popup_id = source.task.popup_id();
|
||||
let document = crate::dom_parser::parse_child_document_projection_from_source(
|
||||
scope,
|
||||
source.document_url.clone(),
|
||||
source.markup,
|
||||
source.content_type,
|
||||
source.character_set,
|
||||
crate::parser::HtmlParser::with_scripting_enabled(
|
||||
self.lightweight_popup_scripting_enabled(popup_id),
|
||||
),
|
||||
)?;
|
||||
let host_ptr = self as *mut JsContextHost;
|
||||
let document_base_url = lightweight_popup_parsed_document_base_url(
|
||||
host_ptr,
|
||||
scope,
|
||||
document,
|
||||
source.document_url,
|
||||
);
|
||||
if let Some(document_record) = self.lightweight_popup_document_record_mut(popup_id) {
|
||||
document_record.state.base_url = document_base_url.clone();
|
||||
}
|
||||
// The committed record owns the navigation-derived referrer. The
|
||||
// response policy container only carries response-derived policy and
|
||||
// can still have an empty referrer here.
|
||||
let document_referrer = self
|
||||
.lightweight_popup_document_record(popup_id)
|
||||
.map(|document| document.state.policy_container.document_referrer.clone())
|
||||
.unwrap_or_default();
|
||||
sync_lightweight_popup_document_window_slots(
|
||||
scope,
|
||||
document,
|
||||
window,
|
||||
&document_base_url,
|
||||
&document_referrer,
|
||||
);
|
||||
set_object_slot(scope, window, "document", document.into());
|
||||
self.forget_lightweight_popup_document_handle(popup_id);
|
||||
let document_handle =
|
||||
self.remember_lightweight_popup_document_handle(scope, popup_id, document);
|
||||
if let Some(document_handle) = document_handle {
|
||||
let _ = crate::context_bootstrap::install_css_runtime_state_for_document(
|
||||
scope,
|
||||
window,
|
||||
Some(document_handle),
|
||||
);
|
||||
install_lightweight_popup_get_computed_style(scope, window, document_handle);
|
||||
}
|
||||
let _ =
|
||||
self.set_lightweight_popup_document_wrapper(popup_id, v8::Global::new(scope, document));
|
||||
let script_advance = self.execute_lightweight_popup_document_scripts(
|
||||
scope,
|
||||
source.task,
|
||||
document_handle,
|
||||
source.policy_container.sandbox.allows_scripts,
|
||||
&source.policy_container.response_content_security_policies,
|
||||
&source
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&source.policy_container.content_security_reporting_endpoints,
|
||||
);
|
||||
let (body_activity, classic_script_load_pending) = match script_advance {
|
||||
LightweightPopupClassicScriptAdvance::Completed(activity) => (activity, false),
|
||||
LightweightPopupClassicScriptAdvance::Pending(activity) => (activity, true),
|
||||
};
|
||||
Some(LightweightPopupDocumentProjectionApplication {
|
||||
document_handle,
|
||||
body_activity,
|
||||
classic_script_load_pending,
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_lightweight_popup_document_scripts(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -3543,9 +3579,10 @@ impl JsContextHost {
|
||||
fn execute_lightweight_popup_window_javascript_url_source(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
popup_id: u64,
|
||||
task: LightweightPopupNavigationTaskToken,
|
||||
source: &str,
|
||||
) -> Result<()> {
|
||||
let popup_id = task.popup_id();
|
||||
let Some(window) = self.lightweight_popup_window(scope, popup_id) else {
|
||||
anyhow::bail!("popup javascript URL has no window");
|
||||
};
|
||||
@@ -3562,7 +3599,7 @@ impl JsContextHost {
|
||||
}
|
||||
self.execute_lightweight_popup_window_javascript_url_source_in_context(
|
||||
popup_scope,
|
||||
popup_id,
|
||||
task,
|
||||
window,
|
||||
source,
|
||||
)
|
||||
@@ -3571,37 +3608,133 @@ impl JsContextHost {
|
||||
fn execute_lightweight_popup_window_javascript_url_source_in_context<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
popup_id: u64,
|
||||
task: LightweightPopupNavigationTaskToken,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
source: &str,
|
||||
) -> Result<()> {
|
||||
let Some(source_value) = v8_string(scope, source) else {
|
||||
let popup_id = task.popup_id();
|
||||
let owner = OwnerDispatchScope::LightweightPopup(popup_id);
|
||||
let Some(source) = self.prepare_javascript_url_source_for_execution(scope, owner, source)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(source_value) = v8_string(scope, &source) else {
|
||||
anyhow::bail!("failed to allocate popup javascript URL source");
|
||||
};
|
||||
let mut script_source = v8::script_compiler::Source::new(source_value, None);
|
||||
let Some(function) = v8::script_compiler::compile_function(
|
||||
let Some(wrapper_source) = v8_string(
|
||||
scope,
|
||||
&mut script_source,
|
||||
&[],
|
||||
&[window],
|
||||
v8::script_compiler::CompileOptions::NoCompileOptions,
|
||||
v8::script_compiler::NoCacheReason::BecauseInlineScript,
|
||||
"(function(window, __moliSource) { with (window) { return eval(__moliSource); } })",
|
||||
) else {
|
||||
anyhow::bail!("v8 failed to compile popup javascript URL");
|
||||
anyhow::bail!("failed to allocate popup javascript URL wrapper");
|
||||
};
|
||||
let Some(function) = v8::Script::compile(scope, wrapper_source, None)
|
||||
.and_then(|script| script.run(scope))
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
else {
|
||||
anyhow::bail!("v8 failed to compile popup javascript URL wrapper");
|
||||
};
|
||||
let previous_popup = enter_active_lightweight_popup_scope(scope, popup_id);
|
||||
let previous_message_source = self.enter_window_message_source_scope(
|
||||
super::PendingWindowMessageEndpoint::LightweightPopup(popup_id),
|
||||
);
|
||||
let run_succeeded = function.call(scope, window.into(), &[]).is_some();
|
||||
let eval_permit = crate::context_bootstrap::arm_internal_javascript_url_eval(scope);
|
||||
let completion = function.call(scope, window.into(), &[window.into(), source_value.into()]);
|
||||
crate::context_bootstrap::restore_internal_javascript_url_eval(scope, eval_permit);
|
||||
self.restore_window_message_source_scope(previous_message_source);
|
||||
restore_active_lightweight_popup_scope(scope, previous_popup);
|
||||
if !run_succeeded {
|
||||
let Some(completion) = completion else {
|
||||
anyhow::bail!("v8 failed to execute popup javascript URL");
|
||||
};
|
||||
if completion.is_string()
|
||||
&& self.lightweight_popup_committed_navigation_task_is_current(task)
|
||||
{
|
||||
let markup = completion
|
||||
.to_string(scope)
|
||||
.map(|value| value.to_rust_string_lossy(scope))
|
||||
.unwrap_or_default();
|
||||
self.commit_lightweight_popup_javascript_url_string_completion(
|
||||
scope, task, window, markup,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit_lightweight_popup_javascript_url_string_completion<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
task: LightweightPopupNavigationTaskToken,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
markup: String,
|
||||
) -> bool {
|
||||
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
|
||||
return false;
|
||||
}
|
||||
let popup_id = task.popup_id();
|
||||
let Some(document_url) = self
|
||||
.lightweight_popup_document_record(popup_id)
|
||||
.map(|document| document.url.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some((document_state, storage_scope)) =
|
||||
self.lightweight_popup_document_commit_seed(popup_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let policy_container = document_state.policy_container.clone();
|
||||
if !self.commit_lightweight_popup_document(
|
||||
scope,
|
||||
window,
|
||||
LightweightPopupDocumentCommit {
|
||||
owner: task.document_owner(),
|
||||
location_url: document_url.clone(),
|
||||
origin: LightweightPopupDocumentCommitOrigin::RetainCurrent,
|
||||
state: document_state,
|
||||
storage_scope,
|
||||
navigation_loader: None,
|
||||
},
|
||||
) || !self.lightweight_popup_committed_navigation_task_is_current(task)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let base_url = self
|
||||
.lightweight_popup_base_url(scope, popup_id)
|
||||
.unwrap_or_else(|| document_url.clone());
|
||||
sync_lightweight_popup_window_location(
|
||||
scope,
|
||||
window,
|
||||
document_url.as_str(),
|
||||
&base_url,
|
||||
&policy_container.document_referrer,
|
||||
);
|
||||
let Some(application) = self.install_lightweight_popup_document_projection(
|
||||
scope,
|
||||
window,
|
||||
LightweightPopupDocumentProjectionSource {
|
||||
task,
|
||||
document_url: &document_url,
|
||||
markup: &markup,
|
||||
content_type: Some("text/html"),
|
||||
character_set: Some("UTF-8"),
|
||||
policy_container: &policy_container,
|
||||
},
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
|
||||
return true;
|
||||
}
|
||||
if !application.classic_script_load_pending {
|
||||
if let Some(document_handle) = application.document_handle {
|
||||
self.sync_child_browsing_context_subtree(scope, document_handle);
|
||||
}
|
||||
if self.lightweight_popup_committed_navigation_task_is_current(task) {
|
||||
self.queue_lightweight_popup_load_event(task);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn queue_lightweight_popup_load_event(&mut self, task: LightweightPopupNavigationTaskToken) {
|
||||
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
|
||||
return;
|
||||
@@ -4658,7 +4791,7 @@ fn lightweight_popup_javascript_url_callback<'s>(
|
||||
return;
|
||||
}
|
||||
if let Err(error) =
|
||||
host.execute_lightweight_popup_window_javascript_url_source(scope, popup_id, &source)
|
||||
host.execute_lightweight_popup_window_javascript_url_source(scope, task, &source)
|
||||
{
|
||||
tracing::debug!(
|
||||
popup_id,
|
||||
|
||||
@@ -8,9 +8,9 @@ use crate::{
|
||||
context_bootstrap::CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
|
||||
document_runtime::{
|
||||
DocumentContentSecurityPolicyCheck, DocumentContentSecurityPolicyViolation,
|
||||
DocumentSubresourceCspKind, DomHandle, create_content_security_policy_violation_event,
|
||||
DocumentPolicyContainer, DocumentSubresourceCspKind, DomHandle,
|
||||
create_content_security_policy_violation_event,
|
||||
},
|
||||
dom::native::Node,
|
||||
native_bridge::{
|
||||
active_child_window_handle, active_lightweight_popup_id,
|
||||
child_window_handle_from_marker_data, entered_child_window_handle,
|
||||
@@ -26,6 +26,15 @@ pub(crate) enum DocumentCspOutcome {
|
||||
SkippedChildContext,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OwnerDocumentPolicySnapshot {
|
||||
document_handle: Option<DomHandle>,
|
||||
document_url: url::Url,
|
||||
policy_container: DocumentPolicyContainer,
|
||||
}
|
||||
|
||||
const JAVASCRIPT_URL_TRUSTED_TYPES_SINK: &str = "Location href";
|
||||
|
||||
fn policy_child_window_handle(scope: &mut v8::PinScope<'_, '_>) -> Option<DomHandle> {
|
||||
if let Some(handle) = active_child_window_handle(scope) {
|
||||
return Some(handle);
|
||||
@@ -49,6 +58,43 @@ impl DocumentCspOutcome {
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
fn owner_document_policy_snapshot(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
) -> Option<OwnerDocumentPolicySnapshot> {
|
||||
match owner {
|
||||
OwnerDispatchScope::Top => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
let runtime = unsafe { &*self.runtime };
|
||||
Some(OwnerDocumentPolicySnapshot {
|
||||
document_handle: Some(runtime.document_handle()),
|
||||
document_url: runtime.document_url().clone(),
|
||||
policy_container: runtime.document_policy_container().clone(),
|
||||
})
|
||||
}
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
let mut policy_container =
|
||||
self.child_browsing_context_policy_container_snapshot(handle)?;
|
||||
policy_container.response_content_security_policies =
|
||||
self.child_effective_response_content_security_policies(handle);
|
||||
policy_container.response_content_security_report_only_policies =
|
||||
self.child_effective_response_content_security_report_only_policies(handle);
|
||||
policy_container.content_security_reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(handle);
|
||||
Some(OwnerDocumentPolicySnapshot {
|
||||
document_handle: self.child_browsing_context_document_handle(handle),
|
||||
document_url: self.child_browsing_context_current_url(handle)?,
|
||||
policy_container,
|
||||
})
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(popup_id) => Some(OwnerDocumentPolicySnapshot {
|
||||
document_handle: self.lightweight_popup_document_handle(popup_id),
|
||||
document_url: self.lightweight_popup_document_url(popup_id)?,
|
||||
policy_container: self.lightweight_popup_policy_container(popup_id)?.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn document_policy_container(
|
||||
&self,
|
||||
) -> &crate::document_runtime::DocumentPolicyContainer {
|
||||
@@ -60,57 +106,91 @@ impl JsContextHost {
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
) -> Option<crate::document_runtime::DocumentConnectPolicySnapshot> {
|
||||
let policy = match owner {
|
||||
OwnerDispatchScope::Top => self.document_policy_container().clone(),
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
self.child_browsing_context_policy_container_snapshot(handle)?
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(popup_id) => {
|
||||
self.lightweight_popup_policy_container(popup_id)?.clone()
|
||||
}
|
||||
};
|
||||
Some(crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(&policy))
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
Some(
|
||||
crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(
|
||||
&snapshot.policy_container,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn trusted_types_for_script_requirements_for_owner(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
) -> Option<TrustedTypesForScriptRequirements> {
|
||||
match owner {
|
||||
OwnerDispatchScope::Top => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(unsafe { &*self.runtime }.trusted_types_for_script_requirements())
|
||||
}
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
let response_policies =
|
||||
self.child_effective_response_content_security_policies(handle);
|
||||
let report_only_policies =
|
||||
self.child_effective_response_content_security_report_only_policies(handle);
|
||||
let reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(handle);
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
|
||||
self.child_browsing_context_document_handle(handle),
|
||||
&response_policies,
|
||||
&report_only_policies,
|
||||
&reporting_endpoints,
|
||||
),
|
||||
)
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(popup_id) => {
|
||||
let policy = self.lightweight_popup_policy_container(popup_id)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
|
||||
self.lightweight_popup_document_handle(popup_id),
|
||||
&policy.response_content_security_policies,
|
||||
&policy.response_content_security_report_only_policies,
|
||||
&policy.content_security_reporting_endpoints,
|
||||
),
|
||||
)
|
||||
}
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
|
||||
snapshot.document_handle,
|
||||
&snapshot.policy_container.response_content_security_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn trusted_types_sink_csp_violations_for_owner(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
sink: &str,
|
||||
sample: &str,
|
||||
) -> Vec<DocumentContentSecurityPolicyViolation> {
|
||||
let Some(snapshot) = self.owner_document_policy_snapshot(owner) else {
|
||||
return Vec::new();
|
||||
};
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.trusted_types_sink_csp_violations_for_document(
|
||||
snapshot.document_handle,
|
||||
&snapshot.document_url,
|
||||
&snapshot.policy_container.response_content_security_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
sink,
|
||||
sample,
|
||||
)
|
||||
}
|
||||
|
||||
/// Run the target Document checks immediately before a `javascript:` URL
|
||||
/// executes. Source-context admission checks may already have happened at
|
||||
/// the navigation API boundary; this is the Chromium-style target check
|
||||
/// shared by top-level, child-frame, and lightweight-popup executors.
|
||||
pub(crate) fn prepare_javascript_url_source_for_execution<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: OwnerDispatchScope,
|
||||
source: &str,
|
||||
) -> Option<String> {
|
||||
let csp_source = format!("javascript:{source}");
|
||||
if !self.allows_inline_javascript_navigation_by_csp(scope, owner, &csp_source) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let requirements = self.trusted_types_for_script_requirements_for_owner(owner)?;
|
||||
let check = crate::context_bootstrap::check_javascript_url_trusted_types(
|
||||
scope,
|
||||
source,
|
||||
requirements,
|
||||
);
|
||||
if check.violated {
|
||||
let host_ptr: *mut JsContextHost = self;
|
||||
self.dispatch_trusted_types_sink_csp_violation_event_for_owner_without_stack_best_effort(
|
||||
scope,
|
||||
host_ptr,
|
||||
owner,
|
||||
JAVASCRIPT_URL_TRUSTED_TYPES_SINK,
|
||||
source,
|
||||
);
|
||||
}
|
||||
check.source
|
||||
}
|
||||
|
||||
pub(crate) fn cross_origin_embedder_policy(
|
||||
@@ -256,7 +336,7 @@ impl JsContextHost {
|
||||
kind: ContentSecurityPolicyNonUrlKind,
|
||||
source: &str,
|
||||
) -> bool {
|
||||
let Some(check) = self.inline_source_csp_check_for_owner(scope, owner, kind, source) else {
|
||||
let Some(check) = self.inline_source_csp_check_for_owner(owner, kind, source) else {
|
||||
// Deliberately fail open only while a child or lightweight popup
|
||||
// has no active document URL/policy context (for example during a
|
||||
// navigation swap). CSP is document-scoped: applying the top-level
|
||||
@@ -282,54 +362,27 @@ impl JsContextHost {
|
||||
|
||||
fn inline_source_csp_check_for_owner(
|
||||
&self,
|
||||
_scope: &mut v8::PinScope<'_, '_>,
|
||||
owner: OwnerDispatchScope,
|
||||
kind: ContentSecurityPolicyNonUrlKind,
|
||||
source: &str,
|
||||
) -> Option<DocumentContentSecurityPolicyCheck> {
|
||||
match owner {
|
||||
OwnerDispatchScope::Top => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(unsafe { &*self.runtime }.inline_source_csp_check(kind, source))
|
||||
}
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
let document_url = self.child_browsing_context_current_url(handle)?;
|
||||
let response_policies =
|
||||
self.child_effective_response_content_security_policies(handle);
|
||||
let response_report_only_policies =
|
||||
self.child_effective_response_content_security_report_only_policies(handle);
|
||||
let response_reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(handle);
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_source_csp_check_for_child_document(
|
||||
self.child_browsing_context_document_handle(handle),
|
||||
&document_url,
|
||||
&response_policies,
|
||||
&response_report_only_policies,
|
||||
&response_reporting_endpoints,
|
||||
kind,
|
||||
source,
|
||||
),
|
||||
)
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(popup_id) => {
|
||||
let document_url = self.lightweight_popup_document_url(popup_id)?;
|
||||
let policy_container = self.lightweight_popup_policy_container(popup_id)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_source_csp_check_for_child_document(
|
||||
self.lightweight_popup_document_handle(popup_id),
|
||||
&document_url,
|
||||
&policy_container.response_content_security_policies,
|
||||
&policy_container.response_content_security_report_only_policies,
|
||||
&policy_container.content_security_reporting_endpoints,
|
||||
kind,
|
||||
source,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_source_csp_check_for_document(
|
||||
snapshot.document_handle,
|
||||
&snapshot.document_url,
|
||||
&snapshot.policy_container.response_content_security_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
kind,
|
||||
source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn inline_script_element_csp_check_for_owner(
|
||||
@@ -338,47 +391,23 @@ impl JsContextHost {
|
||||
source: &str,
|
||||
request: ContentSecurityPolicyScriptElementRequest<'_>,
|
||||
) -> Option<DocumentContentSecurityPolicyCheck> {
|
||||
match owner {
|
||||
OwnerDispatchScope::Top => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(unsafe { &*self.runtime }.inline_script_element_csp_check(source, request))
|
||||
}
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
let document_url = self.child_browsing_context_current_url(handle)?;
|
||||
let response_policies =
|
||||
self.child_effective_response_content_security_policies(handle);
|
||||
let response_report_only_policies =
|
||||
self.child_effective_response_content_security_report_only_policies(handle);
|
||||
let response_reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(handle);
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_script_element_csp_check_for_child_document(
|
||||
self.child_browsing_context_document_handle(handle),
|
||||
&document_url,
|
||||
&response_policies,
|
||||
&response_report_only_policies,
|
||||
&response_reporting_endpoints,
|
||||
source,
|
||||
request,
|
||||
),
|
||||
)
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(popup_id) => {
|
||||
let document_url = self.lightweight_popup_document_url(popup_id)?;
|
||||
let policy_container = self.lightweight_popup_policy_container(popup_id)?;
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_script_element_csp_check_for_child_document(
|
||||
self.lightweight_popup_document_handle(popup_id),
|
||||
&document_url,
|
||||
&policy_container.response_content_security_policies,
|
||||
&policy_container.response_content_security_report_only_policies,
|
||||
&policy_container.content_security_reporting_endpoints,
|
||||
source,
|
||||
request,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.inline_script_element_csp_check_for_document(
|
||||
snapshot.document_handle,
|
||||
&snapshot.document_url,
|
||||
&snapshot.policy_container.response_content_security_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
source,
|
||||
request,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn entered_owner_dispatch_scope(
|
||||
@@ -545,27 +574,18 @@ impl JsContextHost {
|
||||
frame_handle: DomHandle,
|
||||
request_url: &url::Url,
|
||||
) -> Option<DocumentContentSecurityPolicyViolation> {
|
||||
match self.frame_navigation_source_child_handle(frame_handle)? {
|
||||
Some(source_child) => {
|
||||
let document_url = self.child_browsing_context_current_url(source_child)?;
|
||||
let response_policies =
|
||||
self.child_effective_response_content_security_policies(source_child);
|
||||
let response_reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(source_child);
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.document_frame_csp_violation_for_child_document(
|
||||
self.child_browsing_context_document_handle(source_child),
|
||||
&document_url,
|
||||
&response_policies,
|
||||
&response_reporting_endpoints,
|
||||
request_url,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.document_frame_csp_violation(request_url)
|
||||
}
|
||||
}
|
||||
let owner = self.owner_dispatch_scope_for_node(frame_handle)?;
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.document_frame_csp_violation_for_document(
|
||||
snapshot.document_handle,
|
||||
&snapshot.document_url,
|
||||
&snapshot.policy_container.response_content_security_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
request_url,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn frame_navigation_csp_report_only_violation(
|
||||
@@ -573,42 +593,19 @@ impl JsContextHost {
|
||||
frame_handle: DomHandle,
|
||||
request_url: &url::Url,
|
||||
) -> Option<DocumentContentSecurityPolicyViolation> {
|
||||
match self.frame_navigation_source_child_handle(frame_handle)? {
|
||||
Some(source_child) => {
|
||||
let document_url = self.child_browsing_context_current_url(source_child)?;
|
||||
let response_report_only_policies = self
|
||||
.child_effective_response_content_security_report_only_policies(source_child);
|
||||
let response_reporting_endpoints =
|
||||
self.child_effective_content_security_reporting_endpoints(source_child);
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }
|
||||
.document_frame_csp_report_only_violation_for_child_document(
|
||||
&document_url,
|
||||
&response_report_only_policies,
|
||||
&response_reporting_endpoints,
|
||||
request_url,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.document_frame_csp_report_only_violation(request_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_navigation_source_child_handle(
|
||||
&self,
|
||||
frame_handle: DomHandle,
|
||||
) -> Option<Option<DomHandle>> {
|
||||
let owner_document = self
|
||||
.dom_host()
|
||||
.node(frame_handle)
|
||||
.and_then(Node::owner_document)?;
|
||||
if owner_document == self.document_handle() {
|
||||
return Some(None);
|
||||
}
|
||||
self.child_browsing_context_host_for_document_handle(owner_document)
|
||||
.map(Some)
|
||||
let owner = self.owner_dispatch_scope_for_node(frame_handle)?;
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &*self.runtime }.document_frame_csp_report_only_violation_for_document(
|
||||
&snapshot.document_url,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.response_content_security_report_only_policies,
|
||||
&snapshot
|
||||
.policy_container
|
||||
.content_security_reporting_endpoints,
|
||||
request_url,
|
||||
)
|
||||
}
|
||||
|
||||
fn child_effective_response_content_security_policies(
|
||||
@@ -1072,6 +1069,23 @@ impl JsContextHost {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_trusted_types_sink_csp_violation_event_for_owner_without_stack_best_effort<
|
||||
's,
|
||||
>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
owner: OwnerDispatchScope,
|
||||
sink: &str,
|
||||
sample: &str,
|
||||
) {
|
||||
for violation in self.trusted_types_sink_csp_violations_for_owner(owner, sink, sample) {
|
||||
self.dispatch_content_security_policy_violation_event_for_owner_best_effort(
|
||||
scope, host_ptr, owner, &violation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_trusted_types_sink_csp_violation_event_with_location_best_effort<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
@@ -1270,15 +1284,15 @@ impl JsContextHost {
|
||||
frame_handle: DomHandle,
|
||||
violation: &DocumentContentSecurityPolicyViolation,
|
||||
) {
|
||||
match self.frame_navigation_source_child_handle(frame_handle) {
|
||||
Some(Some(source_child)) => {
|
||||
match self.owner_dispatch_scope_for_node(frame_handle) {
|
||||
Some(OwnerDispatchScope::Child(source_child)) => {
|
||||
self.dispatch_child_content_security_policy_violation_event_best_effort(
|
||||
scope,
|
||||
source_child,
|
||||
violation,
|
||||
);
|
||||
}
|
||||
Some(None) => {
|
||||
Some(OwnerDispatchScope::Top) => {
|
||||
let host_ptr: *mut JsContextHost = self;
|
||||
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
|
||||
unsafe { &mut *self.runtime }
|
||||
@@ -1286,6 +1300,11 @@ impl JsContextHost {
|
||||
scope, host_ptr, violation,
|
||||
);
|
||||
}
|
||||
Some(OwnerDispatchScope::LightweightPopup(popup_id)) => {
|
||||
self.dispatch_lightweight_popup_content_security_policy_violation_event_best_effort(
|
||||
scope, popup_id, violation,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
tracing::error!(
|
||||
blocked_uri = violation.blocked_uri.as_str(),
|
||||
|
||||
@@ -628,6 +628,17 @@ struct RuntimeExpressionAwaitSpec {
|
||||
navigation_policy: RuntimeEvaluationNavigationPolicy,
|
||||
}
|
||||
|
||||
struct RendererPostResponseOwnerWork {
|
||||
document_lifecycle: Option<RendererDocumentLifecycleIdentity>,
|
||||
navigation_handoff: Option<RendererTopLevelNavigationHandoff>,
|
||||
}
|
||||
|
||||
impl RendererPostResponseOwnerWork {
|
||||
const fn is_empty(&self) -> bool {
|
||||
self.document_lifecycle.is_none() && self.navigation_handoff.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_live_page_wait_deadline(timeout_ms: u64, operation: &str) -> Result<Instant> {
|
||||
let timeout = std::time::Duration::from_millis(timeout_ms);
|
||||
Instant::now()
|
||||
@@ -1597,7 +1608,7 @@ impl RendererOwnerHandle {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => return Err(error).into(),
|
||||
};
|
||||
entry.begin_standalone_navigation_follow();
|
||||
entry.begin_renderer_navigation_follow();
|
||||
self.restore_live_page_entry(token, entry);
|
||||
}
|
||||
match self
|
||||
@@ -1957,16 +1968,23 @@ impl RendererOwnerHandle {
|
||||
);
|
||||
}
|
||||
|
||||
fn post_response_document_lifecycle_continuation(
|
||||
fn post_response_owner_work_continuation(
|
||||
&self,
|
||||
token: RendererPageToken,
|
||||
document: RendererDocumentLifecycleIdentity,
|
||||
work: RendererPostResponseOwnerWork,
|
||||
) -> RendererPageCommandPostResponseContinuation {
|
||||
let page_wake_tx = self.state.page_wake_tx.clone();
|
||||
RendererPageCommandPostResponseContinuation::new(move || {
|
||||
let _ = page_wake_tx.send(RendererOwnerWake::post_response_document_lifecycle(
|
||||
token, document,
|
||||
));
|
||||
if let Some(document) = work.document_lifecycle {
|
||||
let _ = page_wake_tx.send(RendererOwnerWake::post_response_document_lifecycle(
|
||||
token, document,
|
||||
));
|
||||
}
|
||||
if let Some(handoff) = work.navigation_handoff {
|
||||
let _ = page_wake_tx.send(RendererOwnerWake::top_level_navigation_handoff(
|
||||
token, handoff,
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3380,7 +3398,7 @@ impl RendererOwnerHandle {
|
||||
mut entry: RetiringPageEntry,
|
||||
error: anyhow::Error,
|
||||
) -> RenderRuntimeDispatchOutcome {
|
||||
entry.settle_standalone_navigation_follow(false);
|
||||
entry.settle_renderer_navigation_follow(false);
|
||||
tracing::warn!(
|
||||
page_id = token.page_id.as_u64(),
|
||||
failure = %error,
|
||||
@@ -3407,7 +3425,7 @@ impl RendererOwnerHandle {
|
||||
"retiring page after committed navigation failed to bootstrap"
|
||||
);
|
||||
let cleanup_failure = disposition.to_string();
|
||||
entry.settle_standalone_navigation_follow(false);
|
||||
entry.settle_renderer_navigation_follow(false);
|
||||
remove_page_on_bound_owner_local_store(token);
|
||||
let entry = entry.reject_and_retire(&cleanup_failure);
|
||||
restore_retiring_entry_after_command_on_bound_owner_local_store(token, entry);
|
||||
@@ -3458,7 +3476,7 @@ impl RendererOwnerHandle {
|
||||
);
|
||||
remove_page_on_bound_owner_local_store(token);
|
||||
}
|
||||
entry.settle_standalone_navigation_follow(false);
|
||||
entry.settle_renderer_navigation_follow(false);
|
||||
self.restore_live_page_entry(token, entry);
|
||||
disposition.into_dispatch_outcome(token, page_creation_publication)
|
||||
}
|
||||
@@ -3784,7 +3802,7 @@ impl RendererOwnerHandle {
|
||||
return RenderRuntimeDispatchOutcome::BackgroundComplete(Ok(()));
|
||||
}
|
||||
};
|
||||
let claimed = entry.begin_standalone_navigation_follow_from_handoff(handoff);
|
||||
let claimed = entry.begin_renderer_navigation_follow_from_handoff(handoff);
|
||||
self.restore_live_page_entry(token, entry);
|
||||
if !claimed {
|
||||
tracing::trace!(
|
||||
@@ -4212,7 +4230,7 @@ impl RendererOwnerHandle {
|
||||
action,
|
||||
DocumentLifecycleTurnAction::RequestedTopLevelNavigation { .. }
|
||||
) && entry
|
||||
.begin_standalone_navigation_follow();
|
||||
.begin_renderer_navigation_follow();
|
||||
let delegated_top_level_navigation = if matches!(
|
||||
action,
|
||||
DocumentLifecycleTurnAction::RequestedTopLevelNavigation { .. }
|
||||
@@ -4885,7 +4903,7 @@ impl RendererOwnerHandle {
|
||||
mut entry: LivePageEntry,
|
||||
completion: LivePagePendingNavigationCompletion,
|
||||
) -> RenderRuntimeDispatchOutcome {
|
||||
entry.settle_standalone_navigation_follow(true);
|
||||
entry.settle_renderer_navigation_follow(true);
|
||||
match completion {
|
||||
LivePagePendingNavigationCompletion::Background
|
||||
| LivePagePendingNavigationCompletion::PublishedPageCreation { .. } => {
|
||||
@@ -5001,7 +5019,7 @@ impl RendererOwnerHandle {
|
||||
completion: LivePagePendingNavigationCompletion,
|
||||
download: RendererPendingDownloadActivation,
|
||||
) -> RenderRuntimeDispatchOutcome {
|
||||
entry.settle_standalone_navigation_follow(true);
|
||||
entry.settle_renderer_navigation_follow(true);
|
||||
match completion {
|
||||
LivePagePendingNavigationCompletion::Background
|
||||
| LivePagePendingNavigationCompletion::PublishedPageCreation { .. } => {
|
||||
@@ -5051,7 +5069,7 @@ impl RendererOwnerHandle {
|
||||
completion: LivePagePendingNavigationCompletion,
|
||||
) -> RenderRuntimeDispatchOutcome {
|
||||
if follow_count == 0 {
|
||||
entry.begin_standalone_navigation_follow();
|
||||
entry.begin_renderer_navigation_follow();
|
||||
}
|
||||
self.restore_live_page_entry(token, entry);
|
||||
RenderRuntimeDispatchOutcome::ContinueNextTurn(Box::new(
|
||||
@@ -5450,20 +5468,25 @@ impl RendererOwnerHandle {
|
||||
},
|
||||
);
|
||||
}
|
||||
let post_response_continuation =
|
||||
match replacement_lifecycle.map(|outcome| outcome.readiness) {
|
||||
Some(
|
||||
DocumentLifecycleTurnReadiness::Runnable { document }
|
||||
| DocumentLifecycleTurnReadiness::Blocked { document },
|
||||
) => {
|
||||
if let Err(error) = entry.defer_document_lifecycle_until_response(document) {
|
||||
self.restore_live_page_entry(token, entry);
|
||||
return Err(error).into();
|
||||
}
|
||||
Some(self.post_response_document_lifecycle_continuation(token, document))
|
||||
let deferred_document = match replacement_lifecycle.map(|outcome| outcome.readiness) {
|
||||
Some(
|
||||
DocumentLifecycleTurnReadiness::Runnable { document }
|
||||
| DocumentLifecycleTurnReadiness::Blocked { document },
|
||||
) => {
|
||||
if let Err(error) = entry.defer_document_lifecycle_until_response(document) {
|
||||
self.restore_live_page_entry(token, entry);
|
||||
return Err(error).into();
|
||||
}
|
||||
Some(DocumentLifecycleTurnReadiness::Idle) | None => None,
|
||||
};
|
||||
Some(document)
|
||||
}
|
||||
Some(DocumentLifecycleTurnReadiness::Idle) | None => None,
|
||||
};
|
||||
let post_response_work = RendererPostResponseOwnerWork {
|
||||
document_lifecycle: deferred_document,
|
||||
navigation_handoff: entry.pending_javascript_navigation_handoff(),
|
||||
};
|
||||
let post_response_continuation = (!post_response_work.is_empty())
|
||||
.then(|| self.post_response_owner_work_continuation(token, post_response_work));
|
||||
self.finish_live_page_entry_with_page_state_and_continuation(
|
||||
token,
|
||||
entry,
|
||||
@@ -6086,7 +6109,7 @@ impl RendererOwnerHandle {
|
||||
Err(error) => return Err(error).into(),
|
||||
};
|
||||
if follow_count == 0 {
|
||||
entry.begin_standalone_navigation_follow();
|
||||
entry.begin_renderer_navigation_follow();
|
||||
}
|
||||
let retire_page_on_failure = completion.retires_page_on_navigation_failure();
|
||||
if follow_count >= MAX_PENDING_LOCATION_NAVIGATION_TURNS {
|
||||
|
||||
@@ -233,7 +233,7 @@ impl RendererOwnerHandle {
|
||||
));
|
||||
}
|
||||
if entry.page_vm().vm().has_pending_location_navigation()
|
||||
&& entry.begin_standalone_navigation_follow()
|
||||
&& entry.begin_renderer_navigation_follow()
|
||||
{
|
||||
return self.continue_live_page_pending_navigation(
|
||||
token,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::navigation_follow::CommittedNavigationBootstrapCompletion;
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(super) enum StandaloneNavigationFollowState {
|
||||
pub(super) enum RendererNavigationFollowState {
|
||||
#[default]
|
||||
Idle,
|
||||
Following {
|
||||
@@ -21,7 +21,7 @@ pub(in crate::runtime) struct PublishedReplacementDocument {
|
||||
pub(in crate::runtime) view_generation: u64,
|
||||
}
|
||||
|
||||
impl StandaloneNavigationFollowState {
|
||||
impl RendererNavigationFollowState {
|
||||
pub(super) fn claim(
|
||||
&mut self,
|
||||
current: Option<crate::page_task_queue::RendererTopLevelNavigationHandoff>,
|
||||
@@ -65,7 +65,7 @@ impl StandaloneNavigationFollowState {
|
||||
pub(in crate::runtime) struct LivePageEntry {
|
||||
pub(in crate::runtime) slot: RendererPageSlotHandle,
|
||||
pub(super) top_level_navigation_dispatch: RendererTopLevelNavigationDispatch,
|
||||
pub(super) standalone_navigation_follow: StandaloneNavigationFollowState,
|
||||
pub(super) renderer_navigation_follow: RendererNavigationFollowState,
|
||||
// Keep executable continuation state before `vm`: Rust drops fields in
|
||||
// declaration order, so an exceptional entry drop still releases any
|
||||
// ScriptVm-bound task before releasing the PageVm itself.
|
||||
@@ -180,9 +180,9 @@ impl CommittedNavigationEntry {
|
||||
RetiringPageEntry::new(self.entry)
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
|
||||
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
|
||||
self.entry
|
||||
.standalone_navigation_follow
|
||||
.renderer_navigation_follow
|
||||
.settle(None, succeeded);
|
||||
}
|
||||
}
|
||||
@@ -196,8 +196,8 @@ impl RetiringPageEntry {
|
||||
Self { entry }
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
|
||||
self.entry.settle_standalone_navigation_follow(succeeded);
|
||||
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
|
||||
self.entry.settle_renderer_navigation_follow(succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +210,8 @@ impl std::fmt::Debug for LivePageEntry {
|
||||
&self.top_level_navigation_dispatch,
|
||||
)
|
||||
.field(
|
||||
"standalone_navigation_follow",
|
||||
&self.standalone_navigation_follow,
|
||||
"renderer_navigation_follow",
|
||||
&self.renderer_navigation_follow,
|
||||
)
|
||||
.field("vm", &self.vm)
|
||||
.field(
|
||||
@@ -244,7 +244,7 @@ impl LivePageEntry {
|
||||
slot,
|
||||
top_level_navigation_dispatch:
|
||||
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
|
||||
standalone_navigation_follow: StandaloneNavigationFollowState::Idle,
|
||||
renderer_navigation_follow: RendererNavigationFollowState::Idle,
|
||||
pending_document_lifecycle_turn: None,
|
||||
post_response_document_lifecycle: None,
|
||||
vm: Some(vm),
|
||||
@@ -264,7 +264,7 @@ impl LivePageEntry {
|
||||
slot,
|
||||
top_level_navigation_dispatch:
|
||||
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
|
||||
standalone_navigation_follow: StandaloneNavigationFollowState::Idle,
|
||||
renderer_navigation_follow: RendererNavigationFollowState::Idle,
|
||||
pending_document_lifecycle_turn: None,
|
||||
post_response_document_lifecycle: None,
|
||||
vm: None,
|
||||
@@ -294,45 +294,60 @@ impl LivePageEntry {
|
||||
self.top_level_navigation_dispatch
|
||||
}
|
||||
|
||||
/// Claim the single standalone owner chain for the current pending
|
||||
/// location navigation. A failed chain remains suppressed while that same
|
||||
/// descriptor is pending, so a duplicate producer handoff cannot restart
|
||||
/// the chain with a fresh limit.
|
||||
pub(in crate::runtime) fn begin_standalone_navigation_follow(&mut self) -> bool {
|
||||
self.begin_standalone_navigation_follow_for_handoff(None)
|
||||
pub(in crate::runtime) fn pending_javascript_navigation_handoff(
|
||||
&self,
|
||||
) -> Option<crate::page_task_queue::RendererTopLevelNavigationHandoff> {
|
||||
let vm = self.active_page_vm()?.vm();
|
||||
vm.pending_location_navigation_scheme_is("javascript")
|
||||
.then(|| vm.pending_location_navigation_handoff())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn renderer_owned_pending_navigation_handoff(
|
||||
&self,
|
||||
) -> Option<crate::page_task_queue::RendererTopLevelNavigationHandoff> {
|
||||
if matches!(
|
||||
self.top_level_navigation_dispatch,
|
||||
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter
|
||||
) {
|
||||
return self
|
||||
.active_page_vm()
|
||||
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
|
||||
}
|
||||
self.pending_javascript_navigation_handoff()
|
||||
}
|
||||
|
||||
/// Claim the renderer-owned chain for the current pending location
|
||||
/// navigation. Standalone pages keep every navigation in the renderer;
|
||||
/// delegated pages retain schemes such as `javascript:` that must not
|
||||
/// cross the browser navigation boundary.
|
||||
pub(in crate::runtime) fn begin_renderer_navigation_follow(&mut self) -> bool {
|
||||
self.begin_renderer_navigation_follow_for_handoff(None)
|
||||
}
|
||||
|
||||
/// Claim a producer handoff only while the same request still occupies
|
||||
/// the ScriptVm's unique pending navigation slot. A delayed wake for an
|
||||
/// overwritten request therefore cannot start the replacement request.
|
||||
pub(in crate::runtime) fn begin_standalone_navigation_follow_from_handoff(
|
||||
pub(in crate::runtime) fn begin_renderer_navigation_follow_from_handoff(
|
||||
&mut self,
|
||||
handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
) -> bool {
|
||||
self.begin_standalone_navigation_follow_for_handoff(Some(handoff))
|
||||
self.begin_renderer_navigation_follow_for_handoff(Some(handoff))
|
||||
}
|
||||
|
||||
pub(super) fn begin_standalone_navigation_follow_for_handoff(
|
||||
pub(super) fn begin_renderer_navigation_follow_for_handoff(
|
||||
&mut self,
|
||||
requested: Option<crate::page_task_queue::RendererTopLevelNavigationHandoff>,
|
||||
) -> bool {
|
||||
if !matches!(
|
||||
self.top_level_navigation_dispatch,
|
||||
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let current = self
|
||||
.active_page_vm()
|
||||
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
|
||||
self.standalone_navigation_follow.claim(current, requested)
|
||||
let current = self.renderer_owned_pending_navigation_handoff();
|
||||
self.renderer_navigation_follow.claim(current, requested)
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
|
||||
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
|
||||
let current = self
|
||||
.active_page_vm()
|
||||
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
|
||||
self.standalone_navigation_follow.settle(current, succeeded);
|
||||
self.renderer_navigation_follow.settle(current, succeeded);
|
||||
}
|
||||
|
||||
/// Commit the source Document synchronously. The navigation task guard
|
||||
|
||||
@@ -55,7 +55,7 @@ mod tests;
|
||||
|
||||
pub(in crate::runtime) use bound::*;
|
||||
#[cfg(test)]
|
||||
use entry::StandaloneNavigationFollowState;
|
||||
use entry::RendererNavigationFollowState;
|
||||
pub(in crate::runtime) use entry::{
|
||||
CommittedNavigationEntry, LivePageEntry, PublishedReplacementDocument, RetiringPageEntry,
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ mod navigation_dispatch_tests {
|
||||
}
|
||||
|
||||
fn phase_one_entry_shell_without_active_page_vm(
|
||||
standalone_navigation_follow: StandaloneNavigationFollowState,
|
||||
renderer_navigation_follow: RendererNavigationFollowState,
|
||||
) -> LivePageEntry {
|
||||
let page_id = PageId::new_for_testing(1);
|
||||
let (page_context_cancel_tx, _page_context_cancel_rx) =
|
||||
@@ -102,7 +102,7 @@ mod navigation_dispatch_tests {
|
||||
slot,
|
||||
top_level_navigation_dispatch:
|
||||
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
|
||||
standalone_navigation_follow,
|
||||
renderer_navigation_follow,
|
||||
pending_document_lifecycle_turn: None,
|
||||
post_response_document_lifecycle: None,
|
||||
vm: None,
|
||||
@@ -116,15 +116,15 @@ mod navigation_dispatch_tests {
|
||||
let handoff = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
|
||||
for succeeded in [false, true] {
|
||||
for mut state in [
|
||||
StandaloneNavigationFollowState::Idle,
|
||||
StandaloneNavigationFollowState::Following { handoff },
|
||||
StandaloneNavigationFollowState::FailedWithPendingNavigation { handoff },
|
||||
RendererNavigationFollowState::Idle,
|
||||
RendererNavigationFollowState::Following { handoff },
|
||||
RendererNavigationFollowState::FailedWithPendingNavigation { handoff },
|
||||
] {
|
||||
state.settle(None, succeeded);
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
StandaloneNavigationFollowState::Idle,
|
||||
RendererNavigationFollowState::Idle,
|
||||
"a committed navigation must settle to Idle after succeeded={succeeded}"
|
||||
);
|
||||
}
|
||||
@@ -134,7 +134,7 @@ mod navigation_dispatch_tests {
|
||||
#[test]
|
||||
fn failed_phase_one_advance_returns_a_retiring_entry() {
|
||||
let advance = phase_one::classify_pending_phase_one_entry_advance(
|
||||
phase_one_entry_shell_without_active_page_vm(StandaloneNavigationFollowState::Idle),
|
||||
phase_one_entry_shell_without_active_page_vm(RendererNavigationFollowState::Idle),
|
||||
Err(anyhow!("resume failed")),
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ mod navigation_dispatch_tests {
|
||||
#[test]
|
||||
fn successful_phase_one_advance_without_a_vm_is_retired_as_an_invariant_error() {
|
||||
let advance = phase_one::classify_pending_phase_one_entry_advance(
|
||||
phase_one_entry_shell_without_active_page_vm(StandaloneNavigationFollowState::Idle),
|
||||
phase_one_entry_shell_without_active_page_vm(RendererNavigationFollowState::Idle),
|
||||
Ok(
|
||||
LivePagePendingNavigationPhaseOneAdvance::TriggeredNavigation {
|
||||
stage: PageVmInitStage::DomContentLoaded,
|
||||
@@ -179,14 +179,14 @@ mod navigation_dispatch_tests {
|
||||
fn navigation_handoff_claim_rejects_stale_and_duplicate_requests() {
|
||||
let first = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
|
||||
let second = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(2);
|
||||
let mut state = StandaloneNavigationFollowState::Idle;
|
||||
let mut state = RendererNavigationFollowState::Idle;
|
||||
|
||||
assert!(!state.claim(Some(second), Some(first)));
|
||||
assert_eq!(state, StandaloneNavigationFollowState::Idle);
|
||||
assert_eq!(state, RendererNavigationFollowState::Idle);
|
||||
assert!(state.claim(Some(second), Some(second)));
|
||||
assert_eq!(
|
||||
state,
|
||||
StandaloneNavigationFollowState::Following { handoff: second }
|
||||
RendererNavigationFollowState::Following { handoff: second }
|
||||
);
|
||||
assert!(!state.claim(Some(second), Some(second)));
|
||||
}
|
||||
@@ -195,18 +195,18 @@ mod navigation_dispatch_tests {
|
||||
fn failed_navigation_suppresses_only_the_same_request_identity() {
|
||||
let first = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
|
||||
let second = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(2);
|
||||
let mut state = StandaloneNavigationFollowState::Following { handoff: first };
|
||||
let mut state = RendererNavigationFollowState::Following { handoff: first };
|
||||
|
||||
state.settle(Some(first), false);
|
||||
assert_eq!(
|
||||
state,
|
||||
StandaloneNavigationFollowState::FailedWithPendingNavigation { handoff: first }
|
||||
RendererNavigationFollowState::FailedWithPendingNavigation { handoff: first }
|
||||
);
|
||||
assert!(!state.claim(Some(first), Some(first)));
|
||||
assert!(state.claim(Some(second), Some(second)));
|
||||
assert_eq!(
|
||||
state,
|
||||
StandaloneNavigationFollowState::Following { handoff: second }
|
||||
RendererNavigationFollowState::Following { handoff: second }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1144,7 +1144,18 @@ impl PageVm {
|
||||
);
|
||||
self.vm_mut()
|
||||
.restore_top_level_location_runtime_state(&initiator_url);
|
||||
let replacement_html = self.vm_mut().eval_javascript_url_runtime_turn(&source)?;
|
||||
if self.vm().script_execution_disabled() {
|
||||
return Ok(PageVmFollowNavigationTurnOutcome::Completed);
|
||||
}
|
||||
let replacement_html = self
|
||||
.vm_mut()
|
||||
.eval_javascript_url_runtime_turn(&source, &initiator_url)?;
|
||||
// Chromium suppresses a string completion when the script synchronously
|
||||
// starts a normal navigation or submits a form. That newer navigation
|
||||
// owns the target Document and must win over javascript: replacement.
|
||||
if self.vm().has_pending_location_navigation() {
|
||||
return Ok(PageVmFollowNavigationTurnOutcome::TriggeredNavigation { stage });
|
||||
}
|
||||
if let Some(replacement_html) = replacement_html {
|
||||
self.document_lifecycle.set_next_document_open_start_reason(
|
||||
RendererLifecycleStartReason::JavascriptDocumentReplacement,
|
||||
|
||||
@@ -867,6 +867,18 @@ fn page_diagnostics_snapshot_observes_but_does_not_drain_lifecycle_state() {
|
||||
);
|
||||
}
|
||||
|
||||
async fn follow_pending_javascript_url_for_test(
|
||||
page_vm: &mut PageVm,
|
||||
) -> anyhow::Result<crate::runtime::PageVmFollowNavigationTurnOutcome> {
|
||||
let mut pending_document_lifecycle_turn = None;
|
||||
page_vm
|
||||
.follow_pending_location_navigation_one_turn_async(
|
||||
&mut pending_document_lifecycle_turn,
|
||||
PageVmInitStage::Load,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_location_navigation_executes_when_pending_navigation_is_followed() {
|
||||
run_page_vm_async_test(async move {
|
||||
@@ -878,7 +890,6 @@ async fn javascript_location_navigation_executes_when_pending_navigation_is_foll
|
||||
let (outcome_is_completed, log, href, network_records) = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
let mut pending_document_lifecycle_turn = None;
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__events = [];
|
||||
@@ -892,12 +903,7 @@ globalThis.__events.push('after setter');
|
||||
"javascript: location should remain pending until the owner follows it"
|
||||
);
|
||||
|
||||
let outcome = page_vm
|
||||
.follow_pending_location_navigation_one_turn_async(
|
||||
&mut pending_document_lifecycle_turn,
|
||||
PageVmInitStage::Load,
|
||||
)
|
||||
.await?;
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
let log = page_vm
|
||||
.vm_mut()
|
||||
.eval("JSON.stringify(globalThis.__events)")?;
|
||||
@@ -929,7 +935,240 @@ globalThis.__events.push('after setter');
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_location_assignment_keeps_href_and_drops_pending_when_not_followed() {
|
||||
async fn javascript_location_navigation_exception_is_reported_without_failing_navigation() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-exception.test/start.html").unwrap(),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let (completed, error_count, href) = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__javascriptUrlErrorCount = 0;
|
||||
window.addEventListener("error", event => {
|
||||
globalThis.__javascriptUrlErrorCount += 1;
|
||||
event.preventDefault();
|
||||
});
|
||||
location.href = "javascript:throw new Error('expected javascript URL failure')";
|
||||
"queued"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
let error_count = page_vm
|
||||
.vm_mut()
|
||||
.eval("String(globalThis.__javascriptUrlErrorCount)")?;
|
||||
let href = page_vm.vm_mut().eval("location.href")?;
|
||||
Ok::<_, anyhow::Error>((
|
||||
matches!(
|
||||
outcome,
|
||||
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
|
||||
),
|
||||
error_count,
|
||||
href,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.expect("javascript: exception should not fail the navigation task");
|
||||
|
||||
assert!(completed);
|
||||
assert_eq!(error_count, "1");
|
||||
assert_eq!(
|
||||
href,
|
||||
"https://javascript-location-exception.test/start.html"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_location_navigation_obeys_trusted_types_pre_navigation_check() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-trusted-types.test/start.html").unwrap(),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let ran = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm.vm_mut().set_response_content_security_policies(&[
|
||||
"require-trusted-types-for 'script'".to_owned(),
|
||||
]);
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__blockedJavascriptUrlRan = false;
|
||||
location.href = "javascript:globalThis.__blockedJavascriptUrlRan = true";
|
||||
"queued"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
|
||||
));
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.eval("String(globalThis.__blockedJavascriptUrlRan)")
|
||||
})
|
||||
.await
|
||||
.expect("Trusted Types should block the javascript: task without failing it");
|
||||
|
||||
assert_eq!(ran, "false");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_location_navigation_executes_default_policy_modified_source() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-default-policy.test/start.html").unwrap(),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let observed = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.set_response_content_security_policies(&[
|
||||
"require-trusted-types-for 'script'".to_owned(),
|
||||
]);
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__originalJavascriptUrlRan = false;
|
||||
globalThis.__modifiedJavascriptUrlRan = false;
|
||||
globalThis.__javascriptUrlPolicyCalls = [];
|
||||
trustedTypes.createPolicy("default", {
|
||||
createScript(value, type, sink) {
|
||||
globalThis.__javascriptUrlPolicyCalls.push([value, type, sink]);
|
||||
return value.replace("__originalJavascriptUrlRan", "__modifiedJavascriptUrlRan");
|
||||
}
|
||||
});
|
||||
location.href = "javascript:globalThis.__originalJavascriptUrlRan = true";
|
||||
"queued"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
|
||||
));
|
||||
page_vm.vm_mut().eval(
|
||||
r#"JSON.stringify({
|
||||
original: globalThis.__originalJavascriptUrlRan,
|
||||
modified: globalThis.__modifiedJavascriptUrlRan,
|
||||
calls: globalThis.__javascriptUrlPolicyCalls
|
||||
})"#,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.expect("Trusted Types default policy should rewrite javascript: source");
|
||||
|
||||
assert_eq!(
|
||||
observed,
|
||||
r#"{"original":false,"modified":true,"calls":[["globalThis.__originalJavascriptUrlRan = true","TrustedScript","Location href"]]}"#
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_top_level_javascript_url_does_not_execute_after_scripting_is_disabled() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-disabled.test/start.html").unwrap(),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let (completed, ran) = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__disabledJavascriptUrlRan = false;
|
||||
location.href = "javascript:globalThis.__disabledJavascriptUrlRan = true";
|
||||
"queued"
|
||||
"#,
|
||||
)?;
|
||||
page_vm.vm_mut().set_script_execution_disabled(true);
|
||||
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
let ran = page_vm
|
||||
.vm_mut()
|
||||
.eval("String(globalThis.__disabledJavascriptUrlRan)")?;
|
||||
Ok::<_, anyhow::Error>((
|
||||
matches!(
|
||||
outcome,
|
||||
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
|
||||
),
|
||||
ran,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.expect("disabled javascript: task should complete without execution");
|
||||
|
||||
assert!(completed);
|
||||
assert_eq!(ran, "false");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_string_completion_does_not_replace_after_new_navigation_is_triggered() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-precedence.test/start.html").unwrap(),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let (triggered, pending_url, body_text) = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
document.body.textContent = "original body";
|
||||
location.href = "javascript:(location.href = 'https://javascript-location-precedence.test/winner.html', 'replacement body')";
|
||||
"queued"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
|
||||
let pending = page_vm
|
||||
.vm_mut()
|
||||
.take_pending_location_navigation_with_seed()
|
||||
.expect("javascript: execution should leave the newer navigation pending");
|
||||
let body_text = page_vm.vm_mut().eval("document.body.textContent")?;
|
||||
Ok::<_, anyhow::Error>((
|
||||
matches!(
|
||||
outcome,
|
||||
crate::runtime::PageVmFollowNavigationTurnOutcome::TriggeredNavigation { .. }
|
||||
),
|
||||
pending.url.to_string(),
|
||||
body_text,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.expect("newer navigation should suppress javascript: string replacement");
|
||||
|
||||
assert!(triggered);
|
||||
assert_eq!(
|
||||
pending_url,
|
||||
"https://javascript-location-precedence.test/winner.html"
|
||||
);
|
||||
assert_eq!(body_text, "original body");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn javascript_location_assignment_keeps_href_and_pending_when_not_delegated() {
|
||||
run_page_vm_async_test(async move {
|
||||
let page_vm = test_page_vm_with_document_url(
|
||||
Url::parse("https://javascript-location-ghost.test/start.html").unwrap(),
|
||||
@@ -960,8 +1199,8 @@ location.href
|
||||
"a javascript: pending navigation must never be published to the browser"
|
||||
);
|
||||
assert!(
|
||||
!page_vm.vm().has_pending_location_navigation(),
|
||||
"dropping a javascript: pending navigation must clear the pending record"
|
||||
page_vm.vm().has_pending_location_navigation(),
|
||||
"browser publication must leave renderer-owned javascript: work pending"
|
||||
);
|
||||
let href = page_vm.vm_mut().eval("location.href")?;
|
||||
assert_eq!(
|
||||
|
||||
@@ -1873,7 +1873,7 @@ document.body.appendChild(frame);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate_pages() {
|
||||
async fn javascript_location_assignment_executes_renderer_owned_task_on_delegate_pages() {
|
||||
let runtime = JsRuntime::initialize();
|
||||
let loader =
|
||||
ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("default loader");
|
||||
@@ -1889,8 +1889,14 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
|
||||
|
||||
let (set_reply, _) = page
|
||||
.run_async_command(RendererPageCommand::EvaluateExpression {
|
||||
expression: r#"location.href = "javascript:document.title = 'LOC-RAN'"; "set""#
|
||||
.to_owned(),
|
||||
expression: r#"
|
||||
globalThis.__javascriptLocationDone = new Promise(resolve => {
|
||||
globalThis.__resolveJavascriptLocation = resolve;
|
||||
});
|
||||
location.href = "javascript:document.title = 'LOC-RAN'; globalThis.__resolveJavascriptLocation('ran'); void 0";
|
||||
"set"
|
||||
"#
|
||||
.to_owned(),
|
||||
await_promise: false,
|
||||
})
|
||||
.await
|
||||
@@ -1915,6 +1921,22 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
|
||||
"assigning a javascript: URL must not ghost location.href"
|
||||
);
|
||||
|
||||
let (task_reply, _) = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
page.run_async_command(RendererPageCommand::EvaluateExpression {
|
||||
expression: "globalThis.__javascriptLocationDone".to_owned(),
|
||||
await_promise: true,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("renderer-owned javascript: task should settle its promise")
|
||||
.expect("javascript: task promise should evaluate");
|
||||
assert_eq!(
|
||||
renderer_json_value(task_reply),
|
||||
Some(serde_json::json!("ran")),
|
||||
"the javascript: task must run after the assigning command"
|
||||
);
|
||||
|
||||
let (title_reply, _) = page
|
||||
.run_async_command(RendererPageCommand::EvaluateExpression {
|
||||
expression: "document.title".to_owned(),
|
||||
@@ -1924,8 +1946,7 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
|
||||
.expect("document.title readback should complete");
|
||||
assert_eq!(
|
||||
renderer_json_value(title_reply),
|
||||
Some(serde_json::json!("start")),
|
||||
"a javascript: location assignment must not execute its script"
|
||||
Some(serde_json::json!("LOC-RAN"))
|
||||
);
|
||||
|
||||
page.close_async()
|
||||
|
||||
@@ -5568,11 +5568,6 @@ impl ScriptVm {
|
||||
&mut self,
|
||||
) -> Option<super::native_bridge::PendingLocationNavigation> {
|
||||
if self.pending_location_navigation_scheme_is("javascript") {
|
||||
let source_url = self.document_runtime.document_url().clone();
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
.clear_pending_location_navigation();
|
||||
self.restore_top_level_location_runtime_state(&source_url);
|
||||
return None;
|
||||
}
|
||||
let source_url = self.document_runtime.document_url().clone();
|
||||
|
||||
@@ -652,20 +652,57 @@ impl ScriptVm {
|
||||
pub(crate) fn eval_javascript_url_runtime_turn(
|
||||
&mut self,
|
||||
source: &str,
|
||||
base_url: &Url,
|
||||
) -> Result<Option<String>> {
|
||||
let source = source.to_owned();
|
||||
let base_url = base_url.clone();
|
||||
let context_ptr: *const v8::Global<v8::Context> = &self.page_default_context as *const _;
|
||||
let completion = self
|
||||
.execute_source_text_in_context_ptr_runtime_turn_with_base_url_and_current_window_error_report(
|
||||
context_ptr,
|
||||
source,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
SourceTextScriptCompletionMode::ValueTypeAware,
|
||||
)?;
|
||||
let provenance = CompiledStringProvenance::at_url(base_url.clone());
|
||||
let pending = PendingScriptTurn::new(
|
||||
self.renderer_document_isolate
|
||||
.with_renderer_document_isolate_mut::<
|
||||
RawScriptExecutionResult<Option<SourceTextScriptCompletion>>,
|
||||
>(|isolate| {
|
||||
let scope = 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 host_ptr = context_host_ptr_from_global_bridge(scope).ok_or_else(|| {
|
||||
anyhow!("javascript URL target context has no Window host")
|
||||
})?;
|
||||
let Some(source) = unsafe { &mut *host_ptr }
|
||||
.prepare_javascript_url_source_for_execution(
|
||||
scope,
|
||||
crate::native_bridge::OwnerDispatchScope::Top,
|
||||
&source,
|
||||
)
|
||||
else {
|
||||
Self::perform_microtask_checkpoints(scope, Some(&base_url))?;
|
||||
return Ok(None);
|
||||
};
|
||||
execute_source_text_on_current_stack_with_completion(
|
||||
scope,
|
||||
&source,
|
||||
Some(&provenance),
|
||||
0,
|
||||
None,
|
||||
true,
|
||||
UncaughtScriptReportTarget::CurrentWindow,
|
||||
SourceTextScriptCompletionMode::ValueTypeAware,
|
||||
)
|
||||
.map(Some)
|
||||
}),
|
||||
);
|
||||
let completion = pending
|
||||
.finish_with_style_drain(self, StyleInvalidationTurnExitBoundary::RuntimeEvaluate);
|
||||
let completion = match completion {
|
||||
Ok(Some(completion)) => completion,
|
||||
Ok(None) => return Ok(None),
|
||||
// A javascript: URL reports script exceptions to its Window, but
|
||||
// the navigation itself completes without replacing the Document.
|
||||
Err(RawScriptExecutionError::Exception { .. }) => return Ok(None),
|
||||
Err(RawScriptExecutionError::Internal(error)) => return Err(error),
|
||||
};
|
||||
Ok(match completion {
|
||||
SourceTextScriptCompletion::String(value) => Some(value),
|
||||
SourceTextScriptCompletion::NonString => None,
|
||||
|
||||
@@ -87,7 +87,7 @@ impl ScriptVm {
|
||||
}
|
||||
SourceTextScriptCompletion::NonString => Ok(FrameScriptCompletionValue::NonString),
|
||||
SourceTextScriptCompletion::Ignored => Err(anyhow!(
|
||||
"source-text frame script job unexpectedly ignored completion"
|
||||
"value-aware frame script execution unexpectedly ignored its completion"
|
||||
)),
|
||||
})
|
||||
}
|
||||
@@ -122,7 +122,11 @@ impl ScriptVm {
|
||||
let Some(job) = self.prepare_inline_classic_frame_script_job(job)? else {
|
||||
return Ok(SourceTextScriptCompletion::Ignored);
|
||||
};
|
||||
let mut job = job;
|
||||
let realm_id = self.current_realm_id_for_frame_script_job(&job)?;
|
||||
if !self.prepare_javascript_url_frame_script_job(realm_id, &mut job)? {
|
||||
return Ok(SourceTextScriptCompletion::NonString);
|
||||
}
|
||||
let is_source_text = matches!(&job.source, FrameScriptSource::SourceText(_));
|
||||
let context_ptr = is_source_text
|
||||
.then(|| self.frame_realm_context_ptr(realm_id))
|
||||
@@ -203,6 +207,40 @@ impl ScriptVm {
|
||||
result
|
||||
}
|
||||
|
||||
fn prepare_javascript_url_frame_script_job(
|
||||
&mut self,
|
||||
realm_id: FrameRealmId,
|
||||
job: &mut FrameScriptJob,
|
||||
) -> Result<bool> {
|
||||
if job.kind != crate::frame_owner_model::FrameScriptJobKind::JavascriptUrl {
|
||||
return Ok(true);
|
||||
}
|
||||
let child_handle = self
|
||||
._context_host
|
||||
.borrow()
|
||||
.frame_owner_child_handle_for_script_job(job)
|
||||
.ok_or_else(|| anyhow!("javascript URL frame job has no current child owner"))?;
|
||||
#[cfg(not(test))]
|
||||
let FrameScriptSource::SourceText(source) = &mut job.source;
|
||||
#[cfg(test)]
|
||||
let source = match &mut job.source {
|
||||
FrameScriptSource::SourceText(source) => source,
|
||||
FrameScriptSource::FunctionConstructor(_) => {
|
||||
return Err(anyhow!("javascript URL frame job has no source text"));
|
||||
}
|
||||
};
|
||||
self.with_frame_realm_scope(realm_id, |scope, host_ptr| {
|
||||
let owner = crate::native_bridge::OwnerDispatchScope::Child(child_handle);
|
||||
let Some(checked_source) = unsafe { &mut *host_ptr }
|
||||
.prepare_javascript_url_source_for_execution(scope, owner, source)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
*source = checked_source;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_inline_classic_frame_script_job(
|
||||
&mut self,
|
||||
mut job: FrameScriptJob,
|
||||
|
||||
@@ -151,6 +151,9 @@ pub(super) unsafe extern "C" fn string_code_generation_check_callback(
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return true;
|
||||
};
|
||||
if crate::context_bootstrap::consume_internal_javascript_url_eval(scope) {
|
||||
return true;
|
||||
}
|
||||
let requires_trusted_types_for_script =
|
||||
unsafe { &*host_ptr }.requires_trusted_types_for_script(scope);
|
||||
let action = if requires_trusted_types_for_script {
|
||||
|
||||
@@ -25035,6 +25035,162 @@ async fn lightweight_popup_javascript_url_navigation_runs_async_with_opener() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lightweight_popup_javascript_url_string_completion_replaces_document() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_test_vm_with_loader("https://example.com/base/page.html", &loader);
|
||||
|
||||
let setup = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__stringCompletionPopup = open();
|
||||
__stringCompletionPopup.location.href =
|
||||
"javascript:'<!doctype html><main id=javascript-url-result>replaced</main>'";
|
||||
return `${__stringCompletionPopup.location.href}|${__stringCompletionPopup.document.body.textContent}`;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("popup javascript string completion setup should evaluate");
|
||||
|
||||
assert_eq!(setup, "about:blank|");
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert!(
|
||||
vm.run_next_due_timer_callback_for_test(&loader)
|
||||
.await
|
||||
.expect("popup javascript URL timer should run")
|
||||
);
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r##"JSON.stringify([
|
||||
__stringCompletionPopup.location.href,
|
||||
__stringCompletionPopup.document.querySelector("#javascript-url-result").textContent,
|
||||
__stringCompletionPopup.document.defaultView === __stringCompletionPopup,
|
||||
__stringCompletionPopup.opener === window
|
||||
])"##,
|
||||
)
|
||||
.expect("popup javascript string completion should replace the document"),
|
||||
r#"["about:blank","replaced",true,true]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loaded_lightweight_popup_can_replace_itself_from_javascript_url_string_completion() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm =
|
||||
new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader);
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const html = `<!doctype html><script>
|
||||
open("javascript:'<main id=javascript-url-self-result>self replaced</main>'", "_self");
|
||||
<\/script>`;
|
||||
globalThis.__selfReplacingPopup =
|
||||
open(URL.createObjectURL(new Blob([html], { type: "text/html" })));
|
||||
return __selfReplacingPopup.document.body.textContent;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("self-replacing popup setup should evaluate"),
|
||||
""
|
||||
);
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(Boolean(__selfReplacingPopup.document.querySelector('#javascript-url-self-result')))",
|
||||
"true",
|
||||
"loaded popup javascript URL string completion",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
"__selfReplacingPopup.document.querySelector('#javascript-url-self-result').textContent",
|
||||
)
|
||||
.expect("self-replaced popup document should remain observable"),
|
||||
"self replaced"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn popup_javascript_url_string_document_keeps_inherited_frame_src_policy() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://popup-javascript-url-frame-csp.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.set_response_content_security_policies(&["frame-src 'none'".to_owned()]);
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__popupJavascriptUrlFrameCspEvents = [];
|
||||
addEventListener("message", event => {
|
||||
__popupJavascriptUrlFrameCspEvents.push(String(event.data));
|
||||
});
|
||||
globalThis.__javascriptUrlCspPopup = open();
|
||||
__javascriptUrlCspPopup.addEventListener("securitypolicyviolation", event => {
|
||||
__javascriptUrlCspPopup.opener.postMessage(
|
||||
`${event.violatedDirective}:${event.blockedURI}`, "*");
|
||||
});
|
||||
const markup = '<iframe src="https://blocked-frame.test/fail.html"></iframe>';
|
||||
__javascriptUrlCspPopup.location.href = "javascript:" + JSON.stringify(markup);
|
||||
return __popupJavascriptUrlFrameCspEvents.length;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("popup javascript URL inherited CSP setup should evaluate"),
|
||||
"0"
|
||||
);
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.lightweight_popup_policy_container(1)
|
||||
.expect("popup policy container")
|
||||
.response_content_security_policies,
|
||||
["frame-src 'none'".to_owned()]
|
||||
);
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(Boolean(__javascriptUrlCspPopup.document.querySelector('iframe')))",
|
||||
"true",
|
||||
"popup javascript URL replacement iframe",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"(() => {
|
||||
const frame = __javascriptUrlCspPopup.document.querySelector('iframe');
|
||||
return `${frame.contentWindow !== null}|${frame.contentDocument !== null}`;
|
||||
})()"#,
|
||||
)
|
||||
.expect("popup javascript URL nested frame projection should be observable"),
|
||||
"true|true"
|
||||
);
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(__popupJavascriptUrlFrameCspEvents.length)",
|
||||
"1",
|
||||
"popup javascript URL inherited frame-src violation",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
vm.eval("__popupJavascriptUrlFrameCspEvents.join('|')")
|
||||
.expect("popup javascript URL inherited CSP result should evaluate"),
|
||||
"frame-src:https://blocked-frame.test/fail.html"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("__javascriptUrlCspPopup.location.href")
|
||||
.expect("popup javascript URL replacement should retain its prior URL"),
|
||||
"about:blank"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lightweight_popup_javascript_url_uses_inline_navigation_csp_not_eval_csp() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
@@ -25120,6 +25276,47 @@ async fn lightweight_popup_javascript_url_uses_inline_navigation_csp_not_eval_cs
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lightweight_popup_javascript_url_eval_permit_is_one_shot() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://popup-javascript-url-eval.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.set_response_content_security_policies(&["script-src 'unsafe-inline'".to_owned()]);
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.__popupJavascriptUrlEvalResults = [];
|
||||
onmessage = event => __popupJavascriptUrlEvalResults.push(String(event.data));
|
||||
const popup = open();
|
||||
popup.location.href = `javascript:
|
||||
try {
|
||||
eval("globalThis.__nestedEvalRan = true");
|
||||
opener.postMessage("nested:allowed", "*");
|
||||
} catch (error) {
|
||||
opener.postMessage("nested:" + error.name, "*");
|
||||
}
|
||||
`;
|
||||
"queued"
|
||||
"#,
|
||||
)
|
||||
.expect("popup javascript URL nested eval should queue");
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(__popupJavascriptUrlEvalResults.length)",
|
||||
"1",
|
||||
"popup javascript URL nested eval policy result",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
vm.eval("__popupJavascriptUrlEvalResults.sort().join('|')")
|
||||
.expect("popup javascript URL nested eval result should evaluate"),
|
||||
"nested:EvalError"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lightweight_popup_post_message_round_trips_with_wasm_module() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
|
||||
@@ -5878,6 +5878,77 @@ async fn child_document_open_nested_frame_uses_inherited_frame_src_policy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_javascript_url_nested_frame_uses_inherited_frame_src_policy() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://child-javascript-url-frame-csp.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.document_runtime
|
||||
.set_response_content_security_policies(&["frame-src 'none'".to_owned()]);
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__childJavascriptUrlFrameCspEvents = [];
|
||||
addEventListener("message", event => {
|
||||
__childJavascriptUrlFrameCspEvents.push(String(event.data));
|
||||
});
|
||||
const source = `javascript:
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
parent.postMessage(event.violatedDirective, "*");
|
||||
});
|
||||
const nested = document.createElement("iframe");
|
||||
nested.src = "https://blocked-frame.test/fail.html";
|
||||
document.body.appendChild(nested);
|
||||
`;
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = encodeURI(source.replaceAll("\n", ""));
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
return "queued";
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child javascript URL CSP setup should evaluate"),
|
||||
"queued"
|
||||
);
|
||||
assert!(
|
||||
vm.run_one_child_frame_task_executor_turn(
|
||||
ChildFrameSemanticTurnKind::NavigationCommit,
|
||||
&loader,
|
||||
)
|
||||
.await
|
||||
.expect("javascript URL navigation commit should use the selected-task dispatcher")
|
||||
);
|
||||
run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
|
||||
&mut vm,
|
||||
&loader,
|
||||
ChildFrameSemanticTurnKind::DocumentScriptReady,
|
||||
"javascript URL should execute after its initial child realm materializes",
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
vm.run_one_child_frame_task_executor_turn(
|
||||
ChildFrameSemanticTurnKind::NavigationCommit,
|
||||
&loader,
|
||||
)
|
||||
.await
|
||||
.expect("nested frame navigation should reach the inherited CSP gate")
|
||||
);
|
||||
assert!(
|
||||
vm.run_one_window_message_executor_turn(&loader)
|
||||
.await
|
||||
.expect("child CSP report should dispatch to the top Window")
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(globalThis.__childJavascriptUrlFrameCspEvents)")
|
||||
.expect("child javascript URL CSP events should be observable"),
|
||||
r#"["frame-src"]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_document_write_inline_classic_obeys_inherited_response_csp() {
|
||||
let mut vm = new_storage_test_vm("https://child-document-write-csp.test/");
|
||||
|
||||
Reference in New Issue
Block a user