mirror of
https://github.com/lexmount/moli.git
synced 2026-09-25 00:01:28 +00:00
feat(fetch): add disable-css mode
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user