LibWeb: Refresh blocker CSS after relevant class/id changes
Refresh content blocker styles when connected elements gain or change class or id tokens that can affect generic cosmetic selectors. Cache the class and id tokens covered by each document's content blocker stylesheet, then ask the adblock engine whether newly-added tokens can unlock selectors before invalidating user style for the whole page. This keeps dynamic cosmetic hiding correct without refreshing blocker CSS for unrelated class and id mutations. Add text coverage for irrelevant class/id mutations that should preserve author style invalidation without refreshing blocker CSS. Also prove matching tokens still hide elements.
This commit is contained in:
parent
96bad93bb2
commit
6ea2a4eff5
9 changed files with 236 additions and 27 deletions
|
|
@ -267,6 +267,31 @@ impl GenericSelectorListRules {
|
|||
|
||||
selectors
|
||||
}
|
||||
|
||||
fn has_hidden_selectors_for_class_or_id(
|
||||
&self,
|
||||
classes: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
ids: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
exceptions: &HashSet<String>,
|
||||
) -> bool {
|
||||
for class in classes {
|
||||
if let Some(class_selectors) = self.by_class.get(class.as_ref())
|
||||
&& class_selectors.iter().any(|selector| !exceptions.contains(selector))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
if let Some(id_selectors) = self.by_id.get(id.as_ref())
|
||||
&& id_selectors.iter().any(|selector| !exceptions.contains(selector))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cosmetic_css_for_url(engine: &ContentBlockerEngine, url: &str, classes: &[&str], ids: &[&str]) -> String {
|
||||
|
|
@ -317,6 +342,28 @@ fn cosmetic_css_for_url(engine: &ContentBlockerEngine, url: &str, classes: &[&st
|
|||
css
|
||||
}
|
||||
|
||||
fn has_generic_cosmetic_selectors_for_url(
|
||||
engine: &ContentBlockerEngine,
|
||||
url: &str,
|
||||
classes: &[&str],
|
||||
ids: &[&str],
|
||||
) -> bool {
|
||||
let resources = engine.engine.url_cosmetic_resources(url);
|
||||
if resources.generichide {
|
||||
return false;
|
||||
}
|
||||
|
||||
!engine
|
||||
.engine
|
||||
.hidden_class_id_selectors(classes.iter().copied(), ids.iter().copied(), &resources.exceptions)
|
||||
.is_empty()
|
||||
|| engine.generic_selector_list_rules.has_hidden_selectors_for_class_or_id(
|
||||
classes.iter().copied(),
|
||||
ids.iter().copied(),
|
||||
&resources.exceptions,
|
||||
)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - `rules` and `rules_len` must point to a valid UTF-8 string
|
||||
/// - The returned pointer must be freed with `rust_content_blocker_free`
|
||||
|
|
@ -417,6 +464,39 @@ pub unsafe extern "C" fn rust_content_blocker_cosmetic_css(
|
|||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - `engine` must be null or a valid pointer returned by `rust_content_blocker_create`
|
||||
/// - String pointers and lengths must point to valid UTF-8 strings
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_content_blocker_has_generic_cosmetic_selectors(
|
||||
engine: *const c_void,
|
||||
url: *const u8,
|
||||
url_len: usize,
|
||||
classes: *const u8,
|
||||
classes_len: usize,
|
||||
ids: *const u8,
|
||||
ids_len: usize,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
let Some(engine) = (unsafe { engine_from_raw(engine) }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(url) = (unsafe { string_from_raw(url, url_len) }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(classes) = (unsafe { string_from_raw(classes, classes_len) }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(ids) = (unsafe { string_from_raw(ids, ids_len) }) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let classes: Vec<_> = classes.lines().collect();
|
||||
let ids: Vec<_> = ids.lines().collect();
|
||||
has_generic_cosmetic_selectors_for_url(engine, url, &classes, &ids)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// - `data` and `length` must match a string returned by `rust_content_blocker_cosmetic_css`
|
||||
#[unsafe(no_mangle)]
|
||||
|
|
|
|||
|
|
@ -775,14 +775,68 @@ void Document::visit_edges(Cell::Visitor& visitor)
|
|||
|
||||
String const& Document::content_blocker_style_sheet()
|
||||
{
|
||||
if (!m_content_blocker_style_sheet.has_value())
|
||||
m_content_blocker_style_sheet = ContentBlocker::the().cosmetic_style_sheet_for_document(*this);
|
||||
if (!m_content_blocker_style_sheet.has_value()) {
|
||||
m_content_blocker_style_sheet_checked_classes.clear();
|
||||
m_content_blocker_style_sheet_checked_ids.clear();
|
||||
|
||||
Vector<String> classes;
|
||||
Vector<String> ids;
|
||||
for_each_shadow_including_descendant([&](DOM::Node& node) {
|
||||
auto* element = as_if<DOM::Element>(node);
|
||||
if (!element)
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
if (auto const& id = element->id(); id.has_value()) {
|
||||
auto id_string = id->to_string();
|
||||
if (!id_string.is_empty() && m_content_blocker_style_sheet_checked_ids.set(*id) == AK::HashSetResult::InsertedNewEntry)
|
||||
ids.append(move(id_string));
|
||||
}
|
||||
|
||||
for (auto const& class_name : element->class_names()) {
|
||||
auto class_string = class_name.to_string();
|
||||
if (!class_string.is_empty() && m_content_blocker_style_sheet_checked_classes.set(class_name) == AK::HashSetResult::InsertedNewEntry)
|
||||
classes.append(move(class_string));
|
||||
}
|
||||
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
m_content_blocker_style_sheet = ContentBlocker::the().cosmetic_style_sheet_for_url(fallback_base_url(), classes, ids);
|
||||
}
|
||||
|
||||
return m_content_blocker_style_sheet.value();
|
||||
}
|
||||
|
||||
void Document::invalidate_content_blocker_style_sheet()
|
||||
{
|
||||
m_content_blocker_style_sheet.clear();
|
||||
m_content_blocker_style_sheet_checked_classes.clear();
|
||||
m_content_blocker_style_sheet_checked_ids.clear();
|
||||
}
|
||||
|
||||
bool Document::content_blocker_style_sheet_may_need_refresh_for_class_or_id(FlyString const* id, ReadonlySpan<FlyString> class_names)
|
||||
{
|
||||
if (!m_content_blocker_style_sheet.has_value())
|
||||
return false;
|
||||
|
||||
Vector<String> classes_to_check;
|
||||
Vector<String> ids_to_check;
|
||||
auto append_new_token = [](FlyString const& token, HashTable<FlyString>& checked_tokens, Vector<String>& tokens_to_check) {
|
||||
auto token_string = token.to_string();
|
||||
if (!token_string.is_empty() && checked_tokens.set(token) == AK::HashSetResult::InsertedNewEntry)
|
||||
tokens_to_check.append(move(token_string));
|
||||
};
|
||||
|
||||
if (id)
|
||||
append_new_token(*id, m_content_blocker_style_sheet_checked_ids, ids_to_check);
|
||||
|
||||
for (auto const& class_name : class_names)
|
||||
append_new_token(class_name, m_content_blocker_style_sheet_checked_classes, classes_to_check);
|
||||
|
||||
if (classes_to_check.is_empty() && ids_to_check.is_empty())
|
||||
return false;
|
||||
|
||||
return ContentBlocker::the().has_generic_cosmetic_selectors_for_url(fallback_base_url(), classes_to_check, ids_to_check);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/selection-api/#dom-document-getselection
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/FlyString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/HashTable.h>
|
||||
|
|
@ -1099,6 +1100,7 @@ public:
|
|||
CSS::StyleScope& style_scope() { return m_style_scope; }
|
||||
String const& content_blocker_style_sheet();
|
||||
void invalidate_content_blocker_style_sheet();
|
||||
bool content_blocker_style_sheet_may_need_refresh_for_class_or_id(FlyString const* id, ReadonlySpan<FlyString> class_names);
|
||||
|
||||
void exit_pointer_lock();
|
||||
|
||||
|
|
@ -1469,6 +1471,9 @@ private:
|
|||
ShadowRoot::DocumentShadowRootList m_shadow_roots;
|
||||
|
||||
Optional<String> m_content_blocker_style_sheet;
|
||||
// Class/id tokens already covered by the cached content blocker stylesheet.
|
||||
HashTable<FlyString> m_content_blocker_style_sheet_checked_classes;
|
||||
HashTable<FlyString> m_content_blocker_style_sheet_checked_ids;
|
||||
|
||||
Optional<AK::UnixDateTime> m_last_modified;
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@
|
|||
#include <LibWeb/Layout/ListItemBox.h>
|
||||
#include <LibWeb/Layout/TreeBuilder.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Loader/ContentBlocker.h>
|
||||
#include <LibWeb/MathML/MathMLElement.h>
|
||||
#include <LibWeb/MathML/TagNames.h>
|
||||
#include <LibWeb/Namespace.h>
|
||||
|
|
@ -133,6 +134,20 @@ namespace Web::DOM {
|
|||
|
||||
GC_DEFINE_ALLOCATOR(Element);
|
||||
|
||||
static void invalidate_content_blocker_style_if_needed(Element& element)
|
||||
{
|
||||
if (!element.is_connected())
|
||||
return;
|
||||
if (!ContentBlocker::the().filtering_enabled() || !ContentBlocker::the().has_cosmetic_rules())
|
||||
return;
|
||||
|
||||
auto const& id = element.id();
|
||||
if (!element.document().content_blocker_style_sheet_may_need_refresh_for_class_or_id(id.has_value() ? &id.value() : nullptr, element.class_names()))
|
||||
return;
|
||||
|
||||
element.document().page().invalidate_user_style();
|
||||
}
|
||||
|
||||
Element::Element(Document& document, DOM::QualifiedName qualified_name)
|
||||
: ParentNode(document, NodeType::ELEMENT_NODE)
|
||||
, m_qualified_name(move(qualified_name))
|
||||
|
|
@ -862,6 +877,8 @@ void Element::run_attribute_change_steps(FlyString const& local_name, Optional<S
|
|||
|
||||
if (old_value != value) {
|
||||
CSS::Invalidation::invalidate_style_after_attribute_change(*this, local_name, old_value, value);
|
||||
if (local_name == HTML::AttributeNames::id || local_name == HTML::AttributeNames::class_)
|
||||
invalidate_content_blocker_style_if_needed(*this);
|
||||
document().bump_dom_tree_version();
|
||||
}
|
||||
}
|
||||
|
|
@ -1708,6 +1725,8 @@ void Element::inserted()
|
|||
document().element_with_id_was_added({}, *this);
|
||||
if (m_name.has_value())
|
||||
document().element_with_name_was_added({}, *this);
|
||||
if (m_id.has_value() || !m_classes.is_empty())
|
||||
invalidate_content_blocker_style_if_needed(*this);
|
||||
}
|
||||
|
||||
play_or_cancel_animations_after_display_property_change();
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
#include <AK/Vector.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibWeb/ContentBlockerRustFFI.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/Loader/ContentBlocker.h>
|
||||
|
||||
namespace Web {
|
||||
|
|
@ -232,32 +230,31 @@ String ContentBlocker::cosmetic_style_sheet_for_url(URL::URL const& url, Readonl
|
|||
ids_bytes.length()));
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_document(DOM::Document const& document) const
|
||||
bool ContentBlocker::has_generic_cosmetic_selectors_for_url(URL::URL const& url, ReadonlySpan<String> classes, ReadonlySpan<String> ids) const
|
||||
{
|
||||
if (!filtering_enabled() || !m_engine || !m_has_cosmetic_rules)
|
||||
return {};
|
||||
return false;
|
||||
|
||||
Vector<String> classes;
|
||||
Vector<String> ids;
|
||||
const_cast<DOM::Document&>(document).for_each_shadow_including_descendant([&](DOM::Node& node) {
|
||||
auto* element = as_if<DOM::Element>(node);
|
||||
if (!element)
|
||||
return TraversalDecision::Continue;
|
||||
auto url_string = serialized_url(url);
|
||||
auto classes_string = join_lines(classes);
|
||||
if (classes_string.is_error())
|
||||
return false;
|
||||
|
||||
if (auto const& id = element->id(); id.has_value()) {
|
||||
if (auto id_string = id->to_string(); !id_string.is_empty())
|
||||
ids.append(move(id_string));
|
||||
}
|
||||
auto ids_string = join_lines(ids);
|
||||
if (ids_string.is_error())
|
||||
return false;
|
||||
|
||||
for (auto const& class_name : element->class_names()) {
|
||||
if (auto class_string = class_name.to_string(); !class_string.is_empty())
|
||||
classes.append(move(class_string));
|
||||
}
|
||||
auto classes_bytes = classes_string.value().bytes_as_string_view();
|
||||
auto ids_bytes = ids_string.value().bytes_as_string_view();
|
||||
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
return cosmetic_style_sheet_for_url(document.fallback_base_url(), classes, ids);
|
||||
return ContentBlocking::FFI::rust_content_blocker_has_generic_cosmetic_selectors(
|
||||
m_engine,
|
||||
reinterpret_cast<u8 const*>(url_string.characters()),
|
||||
url_string.length(),
|
||||
reinterpret_cast<u8 const*>(classes_bytes.characters_without_null_termination()),
|
||||
classes_bytes.length(),
|
||||
reinterpret_cast<u8 const*>(ids_bytes.characters_without_null_termination()),
|
||||
ids_bytes.length());
|
||||
}
|
||||
|
||||
ContentBlocker::ResourceType ContentBlocker::resource_type_from_fetch_metadata(Optional<Fetch::Infrastructure::Request::Destination> const& destination, Optional<Fetch::Infrastructure::Request::InitiatorType> const& initiator_type, Fetch::Infrastructure::Request::Mode mode)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
|
|
@ -50,9 +49,9 @@ public:
|
|||
ErrorOr<void> set_patterns(ReadonlySpan<String>);
|
||||
ErrorOr<void> set_rules_from_bytes(ReadonlyBytes);
|
||||
|
||||
String cosmetic_style_sheet_for_document(DOM::Document const&) const;
|
||||
String cosmetic_style_sheet_for_url(URL::URL const&) const;
|
||||
String cosmetic_style_sheet_for_url(URL::URL const&, ReadonlySpan<String> classes, ReadonlySpan<String> ids) const;
|
||||
bool has_generic_cosmetic_selectors_for_url(URL::URL const&, ReadonlySpan<String> classes, ReadonlySpan<String> ids) const;
|
||||
|
||||
static ResourceType resource_type_from_fetch_metadata(Optional<Fetch::Infrastructure::Request::Destination> const&, Optional<Fetch::Infrastructure::Request::InitiatorType> const&, Fetch::Infrastructure::Request::Mode);
|
||||
static URL::URL source_url_for_matching(URL::URL const&);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
block
|
||||
block
|
||||
rgb(1, 2, 3)
|
||||
rgb(4, 5, 6)
|
||||
PASS: irrelevant class/id changes skipped content blocker refresh
|
||||
none
|
||||
none
|
||||
|
|
@ -2,4 +2,4 @@ rgb(0, 0, 0)
|
|||
inline
|
||||
rgb(1, 2, 3)
|
||||
none
|
||||
hasInvalidationRuleCacheBuilds: 0
|
||||
hasInvalidationRuleCacheBuilds: 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="./include.js"></script>
|
||||
<style>
|
||||
.irrelevant {
|
||||
color: rgb(1, 2, 3);
|
||||
}
|
||||
|
||||
#unrelated {
|
||||
color: rgb(4, 5, 6);
|
||||
}
|
||||
</style>
|
||||
<body></body>
|
||||
<script>
|
||||
test(() => {
|
||||
internals.setContentBlockers("##.ad\n###sponsor");
|
||||
|
||||
const classTarget = document.createElement("div");
|
||||
document.body.appendChild(classTarget);
|
||||
|
||||
const idTarget = document.createElement("div");
|
||||
document.body.appendChild(idTarget);
|
||||
|
||||
println(getComputedStyle(classTarget).display);
|
||||
println(getComputedStyle(idTarget).display);
|
||||
|
||||
internals.resetStyleInvalidationCounters();
|
||||
|
||||
classTarget.className = "irrelevant";
|
||||
idTarget.id = "unrelated";
|
||||
|
||||
println(getComputedStyle(classTarget).color);
|
||||
println(getComputedStyle(idTarget).color);
|
||||
|
||||
const counters = internals.getStyleInvalidationCounters();
|
||||
if (counters.fullStyleInvalidations == 0)
|
||||
println("PASS: irrelevant class/id changes skipped content blocker refresh");
|
||||
else
|
||||
println(`FAIL: irrelevant class/id changes caused ${counters.fullStyleInvalidations} full invalidations`);
|
||||
|
||||
classTarget.className = "ad";
|
||||
idTarget.id = "sponsor";
|
||||
|
||||
println(getComputedStyle(classTarget).display);
|
||||
println(getComputedStyle(idTarget).display);
|
||||
|
||||
internals.setContentBlockers("");
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue