feat(fetch): add disable-css mode

This commit is contained in:
ldm0
2026-09-04 03:46:02 +08:00
committed by Donough Liu
parent 9cad6ca50e
commit 2cba5a7aeb
27 changed files with 660 additions and 17 deletions
+19
View File
@@ -12,6 +12,7 @@ pub struct BrowserConfig {
optional_resource_fetch_mask: OptionalResourceFetchMask,
subframe_loading_enabled: bool,
script_execution_disabled: bool,
author_styles_disabled: bool,
wpt_extensions_enabled: bool,
}
@@ -25,6 +26,7 @@ impl Default for BrowserConfig {
optional_resource_fetch_mask: OptionalResourceFetchMask::NONE,
subframe_loading_enabled: true,
script_execution_disabled: false,
author_styles_disabled: false,
wpt_extensions_enabled: false,
}
}
@@ -135,6 +137,14 @@ impl BrowserConfig {
self.script_execution_disabled = disabled;
}
pub fn author_styles_disabled(&self) -> bool {
self.author_styles_disabled
}
pub fn set_author_styles_disabled(&mut self, disabled: bool) {
self.author_styles_disabled = disabled;
}
pub fn wpt_extensions_enabled(&self) -> bool {
self.wpt_extensions_enabled
}
@@ -228,6 +238,15 @@ mod tests {
assert!(config.script_execution_disabled());
}
#[test]
fn browser_config_defaults_to_author_styles_and_can_disable_them() {
let mut config = BrowserConfig::default();
assert!(!config.author_styles_disabled());
config.set_author_styles_disabled(true);
assert!(config.author_styles_disabled());
}
#[test]
fn browser_config_toggles_each_optional_resource_bit_independently() {
for (enabled_type, enabled_bit) in OPTIONAL_RESOURCES {
+1
View File
@@ -251,6 +251,7 @@ impl Browser {
let page_network_policy = PageNetworkPolicy::new(
config.optional_resource_fetch_mask(),
config.subframe_loading_enabled(),
config.author_styles_disabled(),
);
// A script-disabled CLI fetch must not execute site-owned JavaScript
// through a previously registered Service Worker before the new
@@ -580,6 +580,7 @@ impl NavigationEngine {
page_network_policy: PageNetworkPolicy::new(
optional_resource_fetch_mask,
subframe_loading_enabled,
false,
),
layout_policy,
js_runtime,
@@ -629,6 +630,7 @@ impl NavigationEngine {
page_network_policy: PageNetworkPolicy::new(
optional_resource_fetch_mask,
subframe_loading_enabled,
false,
),
layout_policy,
js_runtime: renderer_owner_source.js_runtime.clone(),
+1
View File
@@ -758,6 +758,7 @@ pub(super) struct DocumentRuntime {
document: HostDocumentState,
design_mode_documents: HashSet<DomHandle>,
script_execution_control: crate::script_execution_control::RendererScriptExecutionControl,
author_styles_disabled: bool,
bypass_content_security_policy: bool,
policy_container: DocumentPolicyContainer,
delivered_meta_content_security_policies: RefCell<HashMap<DomHandle, Vec<String>>>,
@@ -1824,6 +1824,9 @@ pub(super) fn finish_runtime_mutation_effects(
);
}
for prepared_owner_change in prepared_owner_changes {
if runtime.author_styles_disabled() {
continue;
}
let owner = prepared_owner_change.owner();
if let Some(url) = prepared_owner_change.cached_linked_stylesheet_url() {
let _ = unsafe { &mut *host_ptr }
@@ -75,6 +75,7 @@ impl DocumentRuntime {
document,
design_mode_documents: HashSet::new(),
script_execution_control: Default::default(),
author_styles_disabled: false,
bypass_content_security_policy: false,
policy_container: DocumentPolicyContainer::default(),
delivered_meta_content_security_policies: RefCell::new(HashMap::new()),
@@ -161,6 +162,7 @@ impl DocumentRuntime {
.script_lifecycle
.retain_standalone_parser_boundary_lifecycle_source(parser_boundary_lifecycle_source);
if let Some((loader, task_runner)) = resource_environment {
runtime.set_author_styles_disabled(loader.author_styles_disabled());
let document_url = runtime
.dom_host
.borrow()
@@ -204,6 +206,7 @@ impl DocumentRuntime {
document: _,
design_mode_documents: _,
script_execution_control: _,
author_styles_disabled: _,
bypass_content_security_policy: _,
policy_container: _,
delivered_meta_content_security_policies: _,
@@ -271,6 +274,14 @@ impl DocumentRuntime {
self.script_execution_control.is_disabled()
}
pub(crate) fn set_author_styles_disabled(&mut self, disabled: bool) {
self.author_styles_disabled = disabled;
}
pub(crate) fn author_styles_disabled(&self) -> bool {
self.author_styles_disabled
}
pub(crate) fn document_scripting_enabled(&self) -> bool {
!self.script_execution_disabled() && self.document_sandbox_policy().allows_scripts
}
@@ -344,7 +344,9 @@ impl JsContextHost {
child_meta_refresh_navigations: HashMap::new(),
disconnected_shadow_roots: HashSet::new(),
live_stylesheets: crate::live_stylesheet::LiveStylesheetRegistry::default(),
style_engine: MoliStyleEngine::new(),
style_engine: MoliStyleEngine::new_with_author_styles_disabled(
runtime.author_styles_disabled(),
),
inline_style_declarations: HashMap::new(),
css_module_texts_by_url: HashMap::new(),
css_module_failed_urls: HashSet::new(),
@@ -1224,6 +1224,9 @@ impl JsContextHost {
&self,
owner: DomHandle,
) -> Option<std::sync::Arc<[crate::style_engine::StylesheetFontFaceDescriptor]>> {
if self.author_styles_disabled() {
return None;
}
let dom_host = self.dom_host();
let element = dom_host.node(owner).and_then(Node::as_element)?;
if !dom_host.is_connected(owner)
@@ -1366,18 +1369,20 @@ impl JsContextHost {
) -> Option<String> {
let document_context = self.style_source_document_context_for_read_document(read_document);
let inputs = inputs.independent_style_engine_projection();
crate::style_engine::MoliStyleEngine::new()
.computed_style_property_value_with_document_context(
self.dom_host(),
&self.document_url_for_handle(read_document),
handle,
property,
None,
&inputs,
document_context.as_ref(),
read_document,
viewport,
)
crate::style_engine::MoliStyleEngine::new_with_author_styles_disabled(
self.style_engine.author_styles_disabled(),
)
.computed_style_property_value_with_document_context(
self.dom_host(),
&self.document_url_for_handle(read_document),
handle,
property,
None,
&inputs,
document_context.as_ref(),
read_document,
viewport,
)
}
fn style_source_document_context(&self) -> OwnedStyleSourceDocumentContext {
@@ -235,6 +235,9 @@ fn document_stylesheet_sources(
context: StyleComputationContext,
) -> Vec<StyloStylesheetSource> {
let mut sources = Vec::new();
if runtime.author_styles_disabled() {
return sources;
}
let Some(document) = source_document else {
return sources;
};
@@ -268,6 +271,9 @@ fn shadow_stylesheet_sources(
root: DomHandle,
context: StyleComputationContext,
) -> Vec<StyloStylesheetSource> {
if runtime.author_styles_disabled() {
return Vec::new();
}
#[cfg(test)]
runtime.note_style_world_shadow_scope_materialization_for_test();
let mut sources = active_stylesheet_handles(runtime, root, context.read_document.is_some())
@@ -295,6 +301,9 @@ pub(super) fn active_stylesheet_handles(
root: DomHandle,
include_detached: bool,
) -> Vec<DomHandle> {
if runtime.author_styles_disabled() {
return Vec::new();
}
let mut handles = runtime
.dom_host()
.stylesheet_candidate_handles_for_tree_scope(root)
@@ -98,6 +98,10 @@ impl DocumentResourceLoaderBootstrap {
pub(crate) fn commit(self, context: DocumentFetchContext) -> DocumentResourceLoader {
DocumentResourceLoader::new(self.request_client, self.task_runner, context)
}
pub(crate) fn author_styles_disabled(&self) -> bool {
self.request_client.author_styles_disabled()
}
}
/// Exact backend source selected when a new Document commits.
+62 -1
View File
@@ -32,6 +32,7 @@ struct PageNetworkPolicyState {
blocked_url_patterns: SharedPatternList,
optional_resource_fetch_mask: OptionalResourceFetchMask,
subframe_loading_enabled: bool,
author_styles_disabled: bool,
bypass_service_worker: bool,
cache_disabled: bool,
}
@@ -54,6 +55,7 @@ impl Default for PageNetworkPolicyState {
blocked_url_patterns: Arc::from([]),
optional_resource_fetch_mask: OptionalResourceFetchMask::NONE,
subframe_loading_enabled: true,
author_styles_disabled: false,
bypass_service_worker: false,
cache_disabled: false,
}
@@ -105,10 +107,12 @@ impl PageNetworkPolicy {
pub fn new(
optional_resource_fetch_mask: OptionalResourceFetchMask,
subframe_loading_enabled: bool,
author_styles_disabled: bool,
) -> Self {
let state = PageNetworkPolicyState {
optional_resource_fetch_mask,
subframe_loading_enabled,
author_styles_disabled,
..PageNetworkPolicyState::default()
};
Self {
@@ -162,6 +166,7 @@ impl PageNetworkPolicy {
blocked_url_patterns: snapshot.blocked_url_patterns,
optional_resource_fetch_mask: snapshot.optional_resource_fetch_mask,
subframe_loading_enabled: snapshot.subframe_loading_enabled,
author_styles_disabled: snapshot.author_styles_disabled,
bypass_service_worker: snapshot.bypass_service_worker,
cache_disabled: snapshot.cache_disabled,
})),
@@ -189,6 +194,7 @@ impl PageNetworkPolicy {
blocked_url_patterns: snapshot.blocked_url_patterns,
optional_resource_fetch_mask: snapshot.optional_resource_fetch_mask,
subframe_loading_enabled: snapshot.subframe_loading_enabled,
author_styles_disabled: snapshot.author_styles_disabled,
bypass_service_worker: snapshot.bypass_service_worker,
cache_disabled: snapshot.cache_disabled,
})),
@@ -222,6 +228,7 @@ impl PageNetworkPolicy {
blocked_url_patterns: state.blocked_url_patterns.clone(),
optional_resource_fetch_mask: state.optional_resource_fetch_mask,
subframe_loading_enabled: state.subframe_loading_enabled,
author_styles_disabled: state.author_styles_disabled,
bypass_service_worker: state.bypass_service_worker,
cache_disabled: state.cache_disabled,
}
@@ -322,6 +329,10 @@ impl PageNetworkPolicy {
self.state.lock().subframe_loading_enabled
}
pub fn author_styles_disabled(&self) -> bool {
self.state.lock().author_styles_disabled
}
pub fn set_bypass_service_worker(&self, bypass: bool) {
let mut state = self.state.lock();
if state.bypass_service_worker == bypass {
@@ -362,6 +373,7 @@ pub struct PageNetworkPolicySnapshot {
blocked_url_patterns: SharedPatternList,
optional_resource_fetch_mask: OptionalResourceFetchMask,
subframe_loading_enabled: bool,
author_styles_disabled: bool,
bypass_service_worker: bool,
cache_disabled: bool,
}
@@ -384,6 +396,10 @@ impl PageNetworkPolicySnapshot {
self.subframe_loading_enabled
}
pub fn author_styles_disabled(&self) -> bool {
self.author_styles_disabled
}
pub fn bypass_service_worker(&self) -> bool {
self.bypass_service_worker
}
@@ -409,6 +425,15 @@ impl PageNetworkPolicySnapshot {
if self.blocks_url(&request.url) {
return Err(anyhow!(BLOCKED_BY_CLIENT_ERROR_TEXT));
}
if self.author_styles_disabled
&& matches!(
request.resource_type,
moli_fetch::RequestResourceType::CssStyleSheet
| moli_fetch::RequestResourceType::LatePreloadCssStyleSheet
)
{
return Err(anyhow!(BLOCKED_BY_CLIENT_ERROR_TEXT));
}
if self.cache_disabled {
request = request.with_cache_mode(RequestCacheMode::Bypass);
@@ -454,7 +479,7 @@ mod tests {
#[test]
fn isolated_policy_copy_preserves_values_without_sharing_mutations() {
let policy = PageNetworkPolicy::new(OptionalResourceFetchMask::IMAGE, false);
let policy = PageNetworkPolicy::new(OptionalResourceFetchMask::IMAGE, false, true);
policy.set_extra_http_headers(&[("x-owner".to_owned(), "first".to_owned())]);
let isolated = policy.isolated_copy();
@@ -464,6 +489,7 @@ mod tests {
OptionalResourceFetchMask::IMAGE
);
assert!(!isolated.subframe_loading_enabled());
assert!(isolated.author_styles_disabled());
isolated.set_network_offline(true);
isolated.set_extra_http_headers(&[("x-owner".to_owned(), "second".to_owned())]);
@@ -483,6 +509,41 @@ mod tests {
);
}
#[test]
fn author_style_policy_blocks_only_stylesheet_requests() {
let policy = PageNetworkPolicy::new(OptionalResourceFetchMask::NONE, true, true);
for resource_type in [
moli_fetch::RequestResourceType::CssStyleSheet,
moli_fetch::RequestResourceType::LatePreloadCssStyleSheet,
] {
let error = policy
.snapshot()
.apply_to_request(
Request::get("https://example.test/page.css")
.unwrap()
.with_resource_type(resource_type)
.with_page_network_policy(),
)
.expect_err("disabled author styles must block CSS requests");
assert_eq!(error.to_string(), BLOCKED_BY_CLIENT_ERROR_TEXT);
}
let script = policy
.snapshot()
.apply_to_request(
Request::get("https://example.test/page.js")
.unwrap()
.with_resource_type(moli_fetch::RequestResourceType::Script)
.with_page_network_policy(),
)
.expect("disabling styles must not block scripts");
assert_eq!(
script.resource_type,
moli_fetch::RequestResourceType::Script
);
}
#[test]
fn request_snapshot_does_not_observe_later_policy_mutation() {
let policy = PageNetworkPolicy::default();
@@ -1027,6 +1027,10 @@ impl ResourceRequestClient {
self.page_network_policy.subframe_loading_enabled()
}
pub fn author_styles_disabled(&self) -> bool {
self.page_network_policy.author_styles_disabled()
}
pub fn set_bypass_service_worker(&self, bypass: bool) {
self.page_network_policy.set_bypass_service_worker(bypass);
}
@@ -1520,6 +1520,7 @@ impl PageVmRuntimeHooks {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScannedStylesheetAdmission {
Admitted,
SuppressedByPagePolicy,
DeferredToParser(ScannedStylesheetDeferral),
}
@@ -4451,6 +4452,9 @@ impl PageVm {
request_resource_type: moli_fetch::RequestResourceType,
link_preload: bool,
) -> ScannedStylesheetAdmission {
if self.vm().document_runtime.author_styles_disabled() {
return ScannedStylesheetAdmission::SuppressedByPagePolicy;
}
if self.vm().fetch_subresource_interception_matches(
crate::types::SubresourceResourceType::Stylesheet,
) {
@@ -4490,6 +4494,9 @@ impl PageVm {
link_preload,
) {
Ok(_) => ScannedStylesheetAdmission::Admitted,
Err(
crate::document_runtime::OwnerlessStylesheetAdmissionError::AuthorStylesDisabled,
) => ScannedStylesheetAdmission::SuppressedByPagePolicy,
Err(
crate::document_runtime::OwnerlessStylesheetAdmissionError::ContentSecurityPolicy,
) => ScannedStylesheetAdmission::DeferredToParser(
@@ -868,6 +868,7 @@ pub(super) fn admit_stylesheet_preloads(
let timing_started = moli_trace::cdp_nav_timing_enabled().then(std::time::Instant::now);
let discovered_count = requests.len();
let mut admitted_count = 0_usize;
let mut page_policy_suppressed_count = 0_usize;
let mut fetch_interception_count = 0_usize;
let mut media_mismatch_count = 0_usize;
let mut content_security_policy_count = 0_usize;
@@ -880,6 +881,9 @@ pub(super) fn admit_stylesheet_preloads(
request.link_preload,
) {
ScannedStylesheetAdmission::Admitted => admitted_count += 1,
ScannedStylesheetAdmission::SuppressedByPagePolicy => {
page_policy_suppressed_count += 1;
}
ScannedStylesheetAdmission::DeferredToParser(
ScannedStylesheetDeferral::FetchInterception,
) => fetch_interception_count += 1,
@@ -898,6 +902,7 @@ pub(super) fn admit_stylesheet_preloads(
target: "moli_cdp_nav_timing",
discovered_count,
admitted_count,
page_policy_suppressed_count,
fetch_interception_count,
media_mismatch_count,
content_security_policy_count,
+2
View File
@@ -1915,6 +1915,7 @@ impl ScriptVmPageRealmBootstrap {
let stylesheet_task_sender = page_task_tx.stylesheet_task_sender();
let main_parser_continuation_sender = page_task_tx.main_parser_continuation_sender();
let resource_owner_id = crate::resource_owner::ResourceOwnerId::new();
let author_styles_disabled = initial_document_loader_bootstrap.author_styles_disabled();
let mut document_runtime = Box::new(DocumentRuntime::from_main_frame_dom_host(
dom_host,
main_document_owner,
@@ -1923,6 +1924,7 @@ impl ScriptVmPageRealmBootstrap {
stylesheet_task_sender,
main_parser_continuation_sender,
));
document_runtime.set_author_styles_disabled(author_styles_disabled);
document_runtime.set_bypass_content_security_policy(bypass_content_security_policy);
let (page_context_cancel_tx, page_context_cancel_rx) =
renderer_page_context_cancel_channel();
@@ -1222,6 +1222,7 @@ fn ensure_retained_style_system_for_computed_read(
engine.invalidation_cleanup_for_world(world),
key,
inputs,
engine.author_styles_disabled(),
);
}
+10
View File
@@ -162,6 +162,7 @@ pub(crate) use world_update::{
pub(crate) struct MoliStyleEngine {
dom_adapter: StyloDomStyleAdapter,
document_worlds: DocumentStyleWorlds,
author_styles_disabled: bool,
owner_stylesheet_source_documents: RefCell<HashMap<DomHandle, DomHandle>>,
linked_stylesheet_owner_documents: RefCell<HashMap<DomHandle, DomHandle>>,
inline_style_metadata_documents: RefCell<HashMap<DomHandle, DomHandle>>,
@@ -175,16 +176,25 @@ impl Default for MoliStyleEngine {
impl MoliStyleEngine {
pub(crate) fn new() -> Self {
Self::new_with_author_styles_disabled(false)
}
pub(crate) fn new_with_author_styles_disabled(author_styles_disabled: bool) -> Self {
ensure_stylo_browser_compat_prefs();
Self {
dom_adapter: StyloDomStyleAdapter::new(),
document_worlds: DocumentStyleWorlds::new(),
author_styles_disabled,
owner_stylesheet_source_documents: RefCell::new(HashMap::new()),
linked_stylesheet_owner_documents: RefCell::new(HashMap::new()),
inline_style_metadata_documents: RefCell::new(HashMap::new()),
}
}
pub(crate) fn author_styles_disabled(&self) -> bool {
self.author_styles_disabled
}
pub(crate) fn author_shared_lock(&self) -> style::shared_lock::SharedRwLock {
self.dom_adapter.shared_lock().clone()
}
@@ -6,7 +6,7 @@ use style::{
servo_arc::Arc as ServoArc,
shared_lock::{SharedRwLock, StylesheetGuards},
stylesheets::{CustomMediaMap, DocumentStyleSheet, Origin, OriginSet, UrlExtraData},
stylist::Stylist,
stylist::{AuthorStylesEnabled, Stylist},
};
use crate::{document_runtime::DomHandle, dom::native::DomHost};
@@ -88,6 +88,7 @@ pub(super) fn build_retained_style_system(
inputs: &FullStyleWorldSnapshot,
shared_lock: &SharedRwLock,
retained_source_records: &[RetainedStylesheetSourceRecord<'_>],
author_styles_disabled: bool,
) -> RetainedStyleSystem {
let mut stylist = new_stylist_with_viewport_bits(
key.viewport_width_bits,
@@ -97,6 +98,11 @@ pub(super) fn build_retained_style_system(
key.environment,
key.quirks_mode,
);
stylist.set_author_styles_enabled(if author_styles_disabled {
AuthorStylesEnabled::No
} else {
AuthorStylesEnabled::Yes
});
register_script_custom_properties(&mut stylist, inputs);
append_stylesheet_to_stylist(
&mut stylist,
@@ -1,5 +1,52 @@
use super::*;
#[test]
fn disabled_author_styles_use_stylo_author_origin_gate() {
let mut host = test_host();
host.reset_html_document_shell();
let body = host.document_body_handle().expect("test body");
let target = host.create_element("div");
assert!(host.set_attribute(target, "class", "styled"));
assert!(host.set_attribute(target, "style", "color: rgb(1, 2, 3); display: none"));
assert!(host.append_child(body, target));
let document_url = url::Url::parse("https://example.test/").expect("test URL");
let mut inputs = FullStyleWorldSnapshot::default();
inputs
.document_stylesheet_sources
.push(StyloStylesheetSource::new(
".styled { color: rgb(4, 5, 6); display: none; }".into(),
document_url.clone(),
));
let engine = MoliStyleEngine::new_with_author_styles_disabled(true);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
target,
"display",
None,
&inputs,
None,
),
Some("block".to_owned()),
"UA styles must remain active while stylesheet and style-attribute declarations are ignored"
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
target,
"color",
None,
&inputs,
None,
),
Some("rgb(0, 0, 0)".to_owned())
);
}
#[test]
fn document_connected_shadow_scope_fallback_roots_include_shadow_roots() {
let mut host = test_host();
@@ -90,6 +90,7 @@ pub(super) fn ensure_retained_style_system(
invalidation_cleanup: StyleInvalidationCleanup<'_>,
key: &StyleWorldKey,
inputs: &FullStyleWorldSnapshot,
author_styles_disabled: bool,
) {
let source_dirty_scope = document_state.source_dirty_scope_snapshot();
if document_state
@@ -174,6 +175,7 @@ pub(super) fn ensure_retained_style_system(
inputs,
&shared_lock,
&retained_source_records,
author_styles_disabled,
);
if trace_enabled {
trace_retained_style_system_change(
@@ -17,6 +17,7 @@ use crate::types::SubresourceResourceType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OwnerlessStylesheetAdmissionError {
AuthorStylesDisabled,
ContentSecurityPolicy,
}
@@ -43,6 +44,9 @@ impl DocumentRuntime {
request_resource_type: moli_fetch::RequestResourceType,
link_preload: bool,
) -> Result<StylesheetFetch, OwnerlessStylesheetAdmissionError> {
if self.author_styles_disabled() {
return Err(OwnerlessStylesheetAdmissionError::AuthorStylesDisabled);
}
let (_, enforced_violation) = self
.response_style_element_request_csp_check(
&request_url,
@@ -142,6 +146,9 @@ impl DocumentRuntime {
document_url: &url::Url,
inputs: impl IntoIterator<Item = DocumentOwnedBlockingStylesheetDiscoveryInput>,
) -> Vec<DocumentOwnedBlockingStylesheetDiscoveryInput> {
if self.author_styles_disabled() {
return Vec::new();
}
let inputs = inputs
.into_iter()
.filter(|input| {
@@ -228,9 +228,15 @@ impl DocumentRuntime {
if self.initial_connected_style_loads_queued {
return Vec::new();
}
self.initial_connected_style_loads_queued = true;
if self.author_styles_disabled() {
self.stylesheet_lifecycle
.pre_initial_scan_processed_owners
.clear();
return Vec::new();
}
self.late_preload_stylesheet_handles =
self.collect_late_preload_stylesheet_handles_for_initial_scan(self.document_handle());
self.initial_connected_style_loads_queued = true;
let prepared = self.prepare_connected_style_loads(self.document_handle(), true);
self.stylesheet_lifecycle
.pre_initial_scan_processed_owners
@@ -309,7 +315,7 @@ impl DocumentRuntime {
root: DomHandle,
skip_existing: bool,
) -> Vec<PreparedConnectedStyleLoad> {
if !self.dom_host.is_connected(root) {
if self.author_styles_disabled() || !self.dom_host.is_connected(root) {
return Vec::new();
}
let load_handles = if self.dom_host.node(root).is_some_and(Node::is_document)
@@ -1431,6 +1437,9 @@ impl DocumentRuntime {
host_ptr: *mut JsContextHost,
owner: DomHandle,
) {
if self.author_styles_disabled() {
return;
}
self.queue_stylesheet_source_css_projection(owner);
if let Some(stylesheet) = unsafe { &*host_ptr }.owner_live_stylesheet(owner) {
self.prime_live_stylesheet_import_loads(owner, stylesheet, false, host_ptr);
@@ -1442,6 +1451,9 @@ impl DocumentRuntime {
host_ptr: *mut JsContextHost,
owner: DomHandle,
) {
if self.author_styles_disabled() {
return;
}
self.queue_stylesheet_source_css_projection(owner);
if let Some(stylesheet) = unsafe { &*host_ptr }.linked_live_stylesheet(owner) {
self.prime_live_stylesheet_import_loads(owner, stylesheet, true, host_ptr);
@@ -1463,6 +1475,10 @@ impl DocumentRuntime {
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
) {
if self.author_styles_disabled() {
self.pending_stylesheet_source_css_projection_owners.clear();
return;
}
let owners = std::mem::take(&mut self.pending_stylesheet_source_css_projection_owners);
for owner in owners {
crate::native_bridge::document::apply_stylesheet_source_css_projection(
+6
View File
@@ -83,6 +83,12 @@ pub struct FetchArgs {
)]
pub disable_js: bool,
/// Disable page-authored CSS before the Document starts parsing. External
/// stylesheets and `@import` resources are not fetched; style/link nodes
/// and style attributes remain in the DOM.
#[arg(long)]
pub disable_css: bool,
#[arg(long)]
pub with_base: bool,
+1
View File
@@ -44,6 +44,7 @@ impl AppConfig {
config
.browser
.set_script_execution_disabled(args.disable_js);
config.browser.set_author_styles_disabled(args.disable_css);
config.fetch.dump_mode = args.dump;
config.fetch.strip = args.strip_options();
config.fetch.with_base = args.with_base;
+35
View File
@@ -35,6 +35,7 @@ fn parses_explicit_fetch_command_with_compatibility_flags() {
"-H",
"X-Trace: two",
"--disable-js",
"--disable-css",
"--with-base",
"--with-frames",
"--strip-mode",
@@ -87,6 +88,7 @@ fn parses_explicit_fetch_command_with_compatibility_flags() {
},
],
disable_js: true,
disable_css: true,
with_base: true,
with_frames: true,
trace_network: false,
@@ -422,6 +424,7 @@ fn infers_fetch_mode_from_bare_url() {
eval_file: None,
headers: vec![],
disable_js: false,
disable_css: false,
with_base: false,
with_frames: false,
trace_network: false,
@@ -464,6 +467,7 @@ fn parses_bare_dump_with_explicit_fetch_command_and_defaults_to_html() {
eval_file: None,
headers: vec![],
disable_js: false,
disable_css: false,
with_base: false,
with_frames: false,
trace_network: false,
@@ -510,6 +514,7 @@ fn parses_header_flag_with_explicit_fetch_command() {
value: "one".to_owned(),
}],
disable_js: false,
disable_css: false,
with_base: false,
with_frames: false,
trace_network: false,
@@ -1476,6 +1481,36 @@ fn app_config_from_fetch_cli_disables_script_execution_only_when_requested() {
);
}
#[test]
fn app_config_from_fetch_cli_disables_author_styles_only_when_requested() {
let enabled = Cli::try_parse_from(normalize_args_for_compat([
"moli",
"fetch",
"https://example.com",
]))
.unwrap();
assert!(
!AppConfig::from_cli(&enabled)
.unwrap()
.browser
.author_styles_disabled()
);
let disabled = Cli::try_parse_from(normalize_args_for_compat([
"moli",
"fetch",
"--disable-css",
"https://example.com",
]))
.unwrap();
assert!(
AppConfig::from_cli(&disabled)
.unwrap()
.browser
.author_styles_disabled()
);
}
#[test]
fn app_config_from_serve_cli_enables_image_fetch() {
let cli = Cli::try_parse_from(normalize_args_for_compat(["moli", "serve", "--image"])).unwrap();
+2
View File
@@ -40,6 +40,8 @@ use tracing_subscriber::fmt::MakeWriter;
#[path = "fetch_cli/anubis_deferred_module.rs"]
mod anubis_deferred_module;
#[path = "fetch_cli/disable_css.rs"]
mod disable_css;
#[path = "fetch_cli/disable_js.rs"]
mod disable_js;
#[path = "fetch_cli/eval.rs"]
+374
View File
@@ -0,0 +1,374 @@
use super::{Output, clean_output, run_fetch_cli_with_args};
use anyhow::Result;
use axum::{
Router,
extract::State,
http::{StatusCode, Uri},
response::{Html, IntoResponse, Response},
routing::get,
};
use parking_lot::Mutex;
use std::sync::Arc;
use tokio::{net::TcpListener, task::JoinHandle};
const CSS_PATHS: [&str; 12] = [
"/external.css",
"/external-import.css",
"/preload.css",
"/inline-import.css",
"/dynamic.css",
"/dynamic-import.css",
"/shadow-import.css",
"/child.css",
"/grandchild.css",
"/srcdoc.css",
"/rewrite.css",
"/rewrite-import.css",
];
const MAIN_PAGE: &str = r#"<!doctype html>
<html>
<head>
<link id="external-link" rel="stylesheet" href="/external.css">
<link id="style-preload" rel="preload" as="style" href="/preload.css">
<style id="inline-sheet">
@import url('/inline-import.css');
#main-target { color: rgb(1, 2, 3); display: none; margin-left: 41px; }
</style>
<script>
document.documentElement.setAttribute('data-script-ran', 'yes');
const constructed = new CSSStyleSheet();
constructed.replaceSync('#adopted-target { color: rgb(7, 8, 9); display: none; }');
document.adoptedStyleSheets = [constructed];
const dynamicStyle = document.createElement('style');
dynamicStyle.textContent = "@import url('/dynamic-import.css'); #dynamic-target { display: none; }";
document.head.appendChild(dynamicStyle);
const dynamicLink = document.createElement('link');
dynamicLink.rel = 'stylesheet';
dynamicLink.href = '/dynamic.css';
document.head.appendChild(dynamicLink);
</script>
</head>
<body>
<div id="main-target" class="external" style="color: rgb(4, 5, 6); display: none">main</div>
<div id="adopted-target">adopted</div>
<div id="dynamic-target">dynamic</div>
<div id="shadow-host"></div>
<iframe id="child" src="/child.html"></iframe>
<iframe id="srcdoc-child" srcdoc="<!doctype html><style>@import url('/srcdoc.css'); #srcdoc-target { display:none; color:rgb(31,32,33) }</style><div id='srcdoc-target' style='display:none'>srcdoc</div><script>document.documentElement.setAttribute('data-script-ran','yes')</script>"></iframe>
<script>
const root = document.getElementById('shadow-host').attachShadow({ mode: 'open' });
root.innerHTML = `<style>@import url('/shadow-import.css'); #shadow-target { display: none; color: rgb(21, 22, 23); }</style><span id="shadow-target" style="display:none">shadow</span>`;
const shadowSheet = new CSSStyleSheet();
shadowSheet.replaceSync('#shadow-target { margin-left: 57px; }');
root.adoptedStyleSheets = [shadowSheet];
</script>
</body>
</html>"#;
const CHILD_PAGE: &str = r#"<!doctype html>
<link rel="stylesheet" href="/child.css">
<style>#child-target { display:none; color:rgb(11,12,13) }</style>
<div id="child-target" style="display:none">child</div>
<iframe id="grandchild" src="/grandchild.html"></iframe>
<script>document.documentElement.setAttribute('data-script-ran', 'yes')</script>"#;
const GRANDCHILD_PAGE: &str = r#"<!doctype html>
<link rel="stylesheet" href="/grandchild.css">
<div id="grandchild-target" style="display:none; color:rgb(14,15,16)">grandchild</div>
<script>document.documentElement.setAttribute('data-script-ran', 'yes')</script>"#;
const DISABLED_READY_SCRIPT: &str = r#"(() => {
const child = document.getElementById('child')?.contentDocument;
const grandchild = child?.getElementById('grandchild')?.contentDocument;
const srcdoc = document.getElementById('srcdoc-child')?.contentDocument;
const shadow = document.getElementById('shadow-host')?.shadowRoot;
const targets = [
[window, document.getElementById('main-target'), 'block'],
[window, document.getElementById('adopted-target'), 'block'],
[window, document.getElementById('dynamic-target'), 'block'],
[window, shadow?.getElementById('shadow-target'), 'inline'],
[child?.defaultView, child?.getElementById('child-target'), 'block'],
[grandchild?.defaultView, grandchild?.getElementById('grandchild-target'), 'block'],
[srcdoc?.defaultView, srcdoc?.getElementById('srcdoc-target'), 'block'],
];
if (!targets.every(([view, target]) => view && target)) {
return false;
}
const authorStylesAreAbsent = targets.every(([view, target, display]) => {
const style = view.getComputedStyle(target);
return style.display === display && style.color === 'rgb(0, 0, 0)';
});
const scriptsRan = [document, child, grandchild, srcdoc].every(
targetDocument => targetDocument?.documentElement?.getAttribute('data-script-ran') === 'yes'
);
if (!authorStylesAreAbsent || !scriptsRan) {
return false;
}
document.documentElement.setAttribute('data-disable-css-probe', 'ready');
return true;
})()"#;
struct DisableCssFixtureServer {
base_url: String,
requests: Arc<Mutex<Vec<String>>>,
task: JoinHandle<()>,
}
impl DisableCssFixtureServer {
async fn spawn() -> Result<Self> {
let requests = Arc::new(Mutex::new(Vec::new()));
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let app = Router::new()
.route("/page.html", get(|| async { Html(MAIN_PAGE) }))
.route("/child.html", get(|| async { Html(CHILD_PAGE) }))
.route("/grandchild.html", get(|| async { Html(GRANDCHILD_PAGE) }))
.route("/baseline.html", get(baseline_page))
.route("/redirect.html", get(redirect_to_page))
.route("/document-open.html", get(document_open_page))
.fallback(css_resource)
.with_state(Arc::clone(&requests));
let task = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("disable-css fixture server should serve");
});
Ok(Self {
base_url: format!("http://{addr}"),
requests,
task,
})
}
fn url(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
fn request_count(&self, path: &str) -> usize {
self.requests
.lock()
.iter()
.filter(|request| request.as_str() == path)
.count()
}
}
impl Drop for DisableCssFixtureServer {
fn drop(&mut self) {
self.task.abort();
}
}
async fn baseline_page() -> Html<&'static str> {
Html(
r#"<!doctype html>
<link rel="stylesheet" href="/external.css">
<div id="main-target" class="external">baseline</div>"#,
)
}
async fn redirect_to_page() -> Response {
(
StatusCode::FOUND,
[("location", "/page.html"), ("cache-control", "no-store")],
"",
)
.into_response()
}
async fn document_open_page() -> Html<&'static str> {
Html(
r#"<!doctype html>
<script>
document.open();
document.write(`<!doctype html>
<link id="rewrite-link" rel="stylesheet" href="/rewrite.css">
<style>@import url('/rewrite-import.css'); #rewrite-target { display:none; color:rgb(91,92,93) }</style>
<div id="rewrite-target" style="display:none; color:rgb(94,95,96)">rewrite</div>`);
document.close();
document.documentElement.setAttribute('data-rewrite-script-ran', 'yes');
</script>"#,
)
}
async fn css_resource(State(requests): State<Arc<Mutex<Vec<String>>>>, uri: Uri) -> Response {
let path = uri.path().to_owned();
requests.lock().push(path.clone());
let body = match path.as_str() {
"/external.css" => {
"@import url('/external-import.css'); .external { color: rgb(51, 52, 53); }"
}
"/external-import.css" => "#main-target { display: none; }",
"/child.css" => "#child-target { color: rgb(61, 62, 63); }",
"/grandchild.css" => "#grandchild-target { color: rgb(71, 72, 73); }",
_ if path.ends_with(".css") => "body { color: rgb(81, 82, 83); }",
_ => return (StatusCode::NOT_FOUND, "not found").into_response(),
};
(
StatusCode::OK,
[
("content-type", "text/css; charset=utf-8"),
("cache-control", "no-store"),
],
body,
)
.into_response()
}
fn assert_success(output: &Output, requests: &[String], scenario: &str) -> String {
assert!(
output.status.success(),
"{scenario} failed: stdout={}\nstderr={}\nrequests={requests:?}",
clean_output(&output.stdout),
clean_output(&output.stderr),
);
clean_output(&output.stdout)
}
fn assert_no_css_requests(server: &DisableCssFixtureServer, scenario: &str) {
for path in CSS_PATHS {
assert_eq!(
server.request_count(path),
0,
"{scenario} unexpectedly requested {path}; requests={:?}",
server.requests.lock().as_slice()
);
}
}
#[test]
fn disables_all_author_style_surfaces_without_disabling_scripts() -> Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
let server = runtime.block_on(DisableCssFixtureServer::spawn())?;
let output = run_fetch_cli_with_args(
&server.url("/page.html"),
&[
"--disable-css",
"--with-frames",
"--timeout",
"5000",
"--wait-script",
DISABLED_READY_SCRIPT,
],
)?;
let stdout = assert_success(
&output,
server.requests.lock().as_slice(),
"disabled author styles",
);
assert!(
stdout.contains("data-disable-css-probe=\"ready\""),
"automation must observe UA-only computed styles: {stdout}"
);
assert!(
stdout.contains("data-script-ran=\"yes\""),
"--disable-css must not disable page JavaScript: {stdout}"
);
for preserved in [
"id=\"external-link\"",
"id=\"inline-sheet\"",
"style=\"color: rgb(4, 5, 6); display: none\"",
] {
assert!(
stdout.contains(preserved),
"disabled CSS must remain represented in the DOM ({preserved}): {stdout}"
);
}
assert_no_css_requests(&server, "--disable-css");
Ok(())
}
#[test]
fn survives_http_redirect_without_losing_the_page_policy() -> Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
let server = runtime.block_on(DisableCssFixtureServer::spawn())?;
let output = run_fetch_cli_with_args(
&server.url("/redirect.html"),
&[
"--disable-css",
"--with-frames",
"--timeout",
"5000",
"--wait-script",
DISABLED_READY_SCRIPT,
],
)?;
let stdout = assert_success(
&output,
server.requests.lock().as_slice(),
"redirected disabled author styles",
);
assert!(stdout.contains("data-disable-css-probe=\"ready\""));
assert_no_css_requests(&server, "redirected --disable-css");
Ok(())
}
#[test]
fn survives_document_open_replacement() -> Result<()> {
const READY_SCRIPT: &str = r#"(() => {
const target = document.getElementById('rewrite-target');
if (!target || document.documentElement.getAttribute('data-rewrite-script-ran') !== 'yes') {
return false;
}
const style = getComputedStyle(target);
const ready = style.display === 'block' && style.color === 'rgb(0, 0, 0)';
if (ready) document.documentElement.setAttribute('data-rewrite-css-probe', 'ready');
return ready;
})()"#;
let runtime = tokio::runtime::Runtime::new()?;
let server = runtime.block_on(DisableCssFixtureServer::spawn())?;
let output = run_fetch_cli_with_args(
&server.url("/document-open.html"),
&[
"--disable-css",
"--timeout",
"5000",
"--wait-script",
READY_SCRIPT,
],
)?;
let stdout = assert_success(
&output,
server.requests.lock().as_slice(),
"document.open disabled author styles",
);
assert!(stdout.contains("data-rewrite-css-probe=\"ready\""));
assert!(stdout.contains("id=\"rewrite-link\""));
assert_no_css_requests(&server, "document.open --disable-css");
Ok(())
}
#[test]
fn author_styles_remain_enabled_by_default() -> Result<()> {
const READY_SCRIPT: &str = r#"(() => {
const target = document.getElementById('main-target');
if (!target) return false;
const ready = getComputedStyle(target).color === 'rgb(51, 52, 53)';
if (ready) document.documentElement.setAttribute('data-css-probe', 'ready');
return ready;
})()"#;
let runtime = tokio::runtime::Runtime::new()?;
let server = runtime.block_on(DisableCssFixtureServer::spawn())?;
let output = run_fetch_cli_with_args(
&server.url("/baseline.html"),
&["--timeout", "5000", "--wait-script", READY_SCRIPT],
)?;
let stdout = assert_success(
&output,
server.requests.lock().as_slice(),
"default author styles",
);
assert!(stdout.contains("data-css-probe=\"ready\""));
assert_eq!(server.request_count("/external.css"), 1);
assert_eq!(server.request_count("/external-import.css"), 1);
Ok(())
}