LibWeb: Use adblock-rust in ContentBlocker
Replace the local substring matcher with the adblock-rust engine exposed through the dedicated content blocker Rust FFI. Keep the previous engine when a replacement list cannot be parsed. Pass shared list buffers directly into Rust instead of building a duplicate C++ vector of lines first. Generate cosmetic CSS through the Rust matcher, including generic class and id selectors collected from shadow-including descendants. Keep a supplemental index for generic cosmetic selector-list rules. adblock-rust indexes these rules under the first class or id token only, so also key them by later simple class and id selectors in the list. Update ContentBlocker coverage for rule options, exceptions, third-party checks, blob and file URLs, invalid list replacement, filtering toggles, cosmetic CSS, and generic selector-list cosmetics.
This commit is contained in:
parent
bcdbdbb5b3
commit
96bad93bb2
7 changed files with 653 additions and 399 deletions
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::c_void;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
|
||||
|
|
@ -13,6 +14,23 @@ pub struct ContentBlockerString {
|
|||
length: usize,
|
||||
}
|
||||
|
||||
struct ContentBlockerEngine {
|
||||
engine: adblock::engine::Engine,
|
||||
generic_selector_list_rules: GenericSelectorListRules,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GenericSelectorListRules {
|
||||
always_needed: HashSet<String>,
|
||||
by_class: HashMap<String, Vec<String>>,
|
||||
by_id: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
enum SelectorKey {
|
||||
Class(String),
|
||||
Id(String),
|
||||
}
|
||||
|
||||
fn abort_on_panic<F: FnOnce() -> R, R>(f: F) -> R {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(result) => result,
|
||||
|
|
@ -45,11 +63,11 @@ unsafe fn string_from_raw<'a>(bytes: *const u8, len: usize) -> Option<&'a str> {
|
|||
std::str::from_utf8(bytes).ok()
|
||||
}
|
||||
|
||||
unsafe fn engine_from_raw<'a>(engine: *const c_void) -> Option<&'a adblock::engine::Engine> {
|
||||
unsafe fn engine_from_raw<'a>(engine: *const c_void) -> Option<&'a ContentBlockerEngine> {
|
||||
if engine.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { &*engine.cast::<adblock::engine::Engine>() })
|
||||
Some(unsafe { &*engine.cast::<ContentBlockerEngine>() })
|
||||
}
|
||||
|
||||
fn string_to_ffi(string: String) -> ContentBlockerString {
|
||||
|
|
@ -67,17 +85,205 @@ fn string_to_ffi(string: String) -> ContentBlockerString {
|
|||
ContentBlockerString { data, length }
|
||||
}
|
||||
|
||||
fn cosmetic_css_for_url(
|
||||
engine: &adblock::engine::Engine,
|
||||
url: &str,
|
||||
classes: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
ids: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
) -> String {
|
||||
let resources = engine.url_cosmetic_resources(url);
|
||||
fn for_each_selector_list_item(selector: &str, mut callback: impl FnMut(&str)) {
|
||||
let mut start = 0;
|
||||
let mut parenthesis_depth: u32 = 0;
|
||||
let mut bracket_depth: u32 = 0;
|
||||
let mut quote = None;
|
||||
let mut escaped = false;
|
||||
|
||||
for (index, character) in selector.char_indices() {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if character == '\\' {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(quote_character) = quote {
|
||||
if character == quote_character {
|
||||
quote = None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match character {
|
||||
'"' | '\'' => quote = Some(character),
|
||||
'(' => parenthesis_depth += 1,
|
||||
')' => parenthesis_depth = parenthesis_depth.saturating_sub(1),
|
||||
'[' => bracket_depth += 1,
|
||||
']' => bracket_depth = bracket_depth.saturating_sub(1),
|
||||
',' if parenthesis_depth == 0 && bracket_depth == 0 => {
|
||||
callback(&selector[start..index]);
|
||||
start = index + character.len_utf8();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
callback(&selector[start..]);
|
||||
}
|
||||
|
||||
fn selector_key_from_start(selector: &str) -> Option<SelectorKey> {
|
||||
let selector = selector.trim_start();
|
||||
let marker = selector.chars().next()?;
|
||||
if marker != '.' && marker != '#' {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut end = marker.len_utf8();
|
||||
for (offset, character) in selector[end..].char_indices() {
|
||||
if character == '\\' {
|
||||
return None;
|
||||
}
|
||||
if !character.is_alphanumeric() && character != '_' && character != '-' {
|
||||
break;
|
||||
}
|
||||
end = marker.len_utf8() + offset + character.len_utf8();
|
||||
}
|
||||
|
||||
if end == marker.len_utf8() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key = selector[marker.len_utf8()..end].to_string();
|
||||
match marker {
|
||||
'.' => Some(SelectorKey::Class(key)),
|
||||
'#' => Some(SelectorKey::Id(key)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericSelectorListRules {
|
||||
fn from_rules(rules: &str) -> Self {
|
||||
let mut selector_list_rules = Self::default();
|
||||
|
||||
for line in rules.lines() {
|
||||
let trimmed_line = line.trim();
|
||||
if trimmed_line.is_empty() || trimmed_line.starts_with('!') || trimmed_line.starts_with('[') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(filter) = adblock::filters::cosmetic::CosmeticFilter::parse(
|
||||
trimmed_line,
|
||||
false,
|
||||
adblock::resources::PermissionMask::default(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let generic_filter = if filter.has_hostname_constraint() {
|
||||
filter.hidden_generic_rule()
|
||||
} else {
|
||||
Some(filter)
|
||||
};
|
||||
|
||||
let Some(generic_filter) = generic_filter else {
|
||||
continue;
|
||||
};
|
||||
let Some(selector) = generic_filter.plain_css_selector() else {
|
||||
continue;
|
||||
};
|
||||
selector_list_rules.add_selector(selector);
|
||||
}
|
||||
|
||||
selector_list_rules
|
||||
}
|
||||
|
||||
fn add_selector(&mut self, selector: &str) {
|
||||
let mut selector_list_item_count = 0;
|
||||
let mut has_unkeyable_item = false;
|
||||
let mut class_keys = HashSet::new();
|
||||
let mut id_keys = HashSet::new();
|
||||
for_each_selector_list_item(selector, |selector_list_item| {
|
||||
selector_list_item_count += 1;
|
||||
|
||||
match selector_key_from_start(selector_list_item) {
|
||||
Some(SelectorKey::Class(class)) => {
|
||||
class_keys.insert(class);
|
||||
}
|
||||
Some(SelectorKey::Id(id)) => {
|
||||
id_keys.insert(id);
|
||||
}
|
||||
None => has_unkeyable_item = true,
|
||||
}
|
||||
});
|
||||
|
||||
if selector_list_item_count < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let selector = selector.to_string();
|
||||
if has_unkeyable_item {
|
||||
self.always_needed.insert(selector);
|
||||
return;
|
||||
}
|
||||
|
||||
for class in class_keys {
|
||||
self.by_class.entry(class).or_default().push(selector.clone());
|
||||
}
|
||||
for id in id_keys {
|
||||
self.by_id.entry(id).or_default().push(selector.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn hidden_selectors(
|
||||
&self,
|
||||
classes: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
ids: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
exceptions: &HashSet<String>,
|
||||
) -> Vec<String> {
|
||||
let mut selectors: Vec<_> = self
|
||||
.always_needed
|
||||
.iter()
|
||||
.filter(|selector| !exceptions.contains(*selector))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for class in classes {
|
||||
if let Some(class_selectors) = self.by_class.get(class.as_ref()) {
|
||||
selectors.extend(
|
||||
class_selectors
|
||||
.iter()
|
||||
.filter(|selector| !exceptions.contains(*selector))
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
if let Some(id_selectors) = self.by_id.get(id.as_ref()) {
|
||||
selectors.extend(
|
||||
id_selectors
|
||||
.iter()
|
||||
.filter(|selector| !exceptions.contains(*selector))
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
selectors
|
||||
}
|
||||
}
|
||||
|
||||
fn cosmetic_css_for_url(engine: &ContentBlockerEngine, url: &str, classes: &[&str], ids: &[&str]) -> String {
|
||||
let resources = engine.engine.url_cosmetic_resources(url);
|
||||
let mut selectors = resources.hide_selectors;
|
||||
|
||||
if !resources.generichide {
|
||||
selectors.extend(engine.hidden_class_id_selectors(classes, ids, &resources.exceptions));
|
||||
selectors.extend(engine.engine.hidden_class_id_selectors(
|
||||
classes.iter().copied(),
|
||||
ids.iter().copied(),
|
||||
&resources.exceptions,
|
||||
));
|
||||
selectors.extend(engine.generic_selector_list_rules.hidden_selectors(
|
||||
classes.iter().copied(),
|
||||
ids.iter().copied(),
|
||||
&resources.exceptions,
|
||||
));
|
||||
}
|
||||
|
||||
let mut selector_styles: Vec<_> = selectors
|
||||
|
|
@ -122,6 +328,11 @@ pub unsafe extern "C" fn rust_content_blocker_create(rules: *const u8, rules_len
|
|||
};
|
||||
|
||||
let engine = adblock::engine::Engine::from_rules(rules.lines(), adblock::lists::ParseOptions::default());
|
||||
let generic_selector_list_rules = GenericSelectorListRules::from_rules(rules);
|
||||
let engine = ContentBlockerEngine {
|
||||
engine,
|
||||
generic_selector_list_rules,
|
||||
};
|
||||
Box::into_raw(Box::new(engine)).cast()
|
||||
})
|
||||
}
|
||||
|
|
@ -132,7 +343,7 @@ pub unsafe extern "C" fn rust_content_blocker_create(rules: *const u8, rules_len
|
|||
pub unsafe extern "C" fn rust_content_blocker_free(engine: *mut c_void) {
|
||||
abort_on_panic(|| {
|
||||
if !engine.is_null() {
|
||||
drop(unsafe { Box::from_raw(engine.cast::<adblock::engine::Engine>()) });
|
||||
drop(unsafe { Box::from_raw(engine.cast::<ContentBlockerEngine>()) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -168,7 +379,7 @@ pub unsafe extern "C" fn rust_content_blocker_matches(
|
|||
return false;
|
||||
};
|
||||
|
||||
engine.check_network_request(&request).matched
|
||||
engine.engine.check_network_request(&request).matched
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +411,9 @@ pub unsafe extern "C" fn rust_content_blocker_cosmetic_css(
|
|||
return string_to_ffi(String::new());
|
||||
};
|
||||
|
||||
string_to_ffi(cosmetic_css_for_url(engine, url, classes.lines(), ids.lines()))
|
||||
let classes: Vec<_> = classes.lines().collect();
|
||||
let ids: Vec<_> = ids.lines().collect();
|
||||
string_to_ffi(cosmetic_css_for_url(engine, url, &classes, &ids))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/BinarySearch.h>
|
||||
#include <AK/Queue.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/Span.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#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 {
|
||||
|
|
@ -24,23 +23,153 @@ ContentBlocker& ContentBlocker::the()
|
|||
|
||||
ContentBlocker::ContentBlocker() = default;
|
||||
|
||||
ContentBlocker::~ContentBlocker() = default;
|
||||
ContentBlocker::~ContentBlocker()
|
||||
{
|
||||
ContentBlocking::FFI::rust_content_blocker_free(m_engine);
|
||||
}
|
||||
|
||||
static StringView resource_type_to_adblock_request_type(ContentBlocker::ResourceType type)
|
||||
{
|
||||
switch (type) {
|
||||
case ContentBlocker::ResourceType::Document:
|
||||
return "document"sv;
|
||||
case ContentBlocker::ResourceType::Font:
|
||||
return "font"sv;
|
||||
case ContentBlocker::ResourceType::Image:
|
||||
return "image"sv;
|
||||
case ContentBlocker::ResourceType::Media:
|
||||
return "media"sv;
|
||||
case ContentBlocker::ResourceType::Object:
|
||||
return "object"sv;
|
||||
case ContentBlocker::ResourceType::Other:
|
||||
return "other"sv;
|
||||
case ContentBlocker::ResourceType::Ping:
|
||||
return "ping"sv;
|
||||
case ContentBlocker::ResourceType::Script:
|
||||
return "script"sv;
|
||||
case ContentBlocker::ResourceType::Stylesheet:
|
||||
return "stylesheet"sv;
|
||||
case ContentBlocker::ResourceType::Subdocument:
|
||||
return "subdocument"sv;
|
||||
case ContentBlocker::ResourceType::WebSocket:
|
||||
return "websocket"sv;
|
||||
case ContentBlocker::ResourceType::XMLHttpRequest:
|
||||
return "xmlhttprequest"sv;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
static ByteString serialized_url(URL::URL const& url)
|
||||
{
|
||||
return url.serialize().to_byte_string();
|
||||
}
|
||||
|
||||
static ByteString serialized_url_for_matching(URL::URL const& url)
|
||||
{
|
||||
if (url.scheme() == "file"sv)
|
||||
return ByteString::formatted("http://local-file.invalid/{}", serialized_url(url));
|
||||
return serialized_url(url);
|
||||
}
|
||||
|
||||
static String take_rust_string(ContentBlocking::FFI::ContentBlockerString rust_string)
|
||||
{
|
||||
if (!rust_string.data)
|
||||
return {};
|
||||
|
||||
ArmedScopeGuard free_string = [&] {
|
||||
ContentBlocking::FFI::rust_content_blocker_free_string(rust_string.data, rust_string.length);
|
||||
};
|
||||
|
||||
auto maybe_string = String::from_utf8({ reinterpret_cast<char const*>(rust_string.data), rust_string.length });
|
||||
if (maybe_string.is_error())
|
||||
return {};
|
||||
return maybe_string.release_value();
|
||||
}
|
||||
|
||||
static ErrorOr<String> join_lines(ReadonlySpan<String> lines)
|
||||
{
|
||||
StringBuilder builder;
|
||||
for (auto const& line : lines) {
|
||||
builder.append(line);
|
||||
builder.append('\n');
|
||||
}
|
||||
return builder.to_string();
|
||||
}
|
||||
|
||||
static bool line_looks_like_supported_cosmetic_rule(StringView line)
|
||||
{
|
||||
auto trimmed_line = line.trim_whitespace();
|
||||
if (trimmed_line.is_empty() || trimmed_line.starts_with('!') || trimmed_line.starts_with('['))
|
||||
return false;
|
||||
|
||||
auto sharp_index = trimmed_line.find('#');
|
||||
if (!sharp_index.has_value())
|
||||
return false;
|
||||
|
||||
auto after_sharp_index = *sharp_index + 1;
|
||||
if (after_sharp_index >= trimmed_line.length())
|
||||
return false;
|
||||
|
||||
auto second_sharp_index = trimmed_line.find('#', after_sharp_index);
|
||||
if (!second_sharp_index.has_value())
|
||||
return false;
|
||||
|
||||
auto between_sharps = trimmed_line.substring_view(after_sharp_index, *second_sharp_index - after_sharp_index);
|
||||
if (between_sharps.starts_with('@')) {
|
||||
if (*sharp_index == 0)
|
||||
return false;
|
||||
between_sharps = between_sharps.substring_view(1);
|
||||
}
|
||||
if (between_sharps.starts_with('?'))
|
||||
between_sharps = between_sharps.substring_view(1);
|
||||
|
||||
return between_sharps.is_empty();
|
||||
}
|
||||
|
||||
static bool rules_contain_cosmetic_rules(ReadonlyBytes rules_bytes)
|
||||
{
|
||||
bool has_cosmetic_rules = false;
|
||||
StringView { rules_bytes }.for_each_split_view('\n', SplitBehavior::Nothing, [&](StringView line) {
|
||||
if (line_looks_like_supported_cosmetic_rule(line))
|
||||
has_cosmetic_rules = true;
|
||||
});
|
||||
return has_cosmetic_rules;
|
||||
}
|
||||
|
||||
ErrorOr<void> ContentBlocker::set_patterns(ReadonlySpan<String> patterns)
|
||||
{
|
||||
StringBuilder builder;
|
||||
for (auto const& pattern : patterns) {
|
||||
if (pattern.is_empty())
|
||||
continue;
|
||||
builder.append(pattern);
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
auto patterns_string = TRY(builder.to_string());
|
||||
auto patterns_bytes = patterns_string.bytes_as_string_view().bytes();
|
||||
return set_rules_from_bytes(patterns_bytes);
|
||||
}
|
||||
|
||||
ErrorOr<void> ContentBlocker::set_rules_from_bytes(ReadonlyBytes rules_bytes)
|
||||
{
|
||||
auto* engine = ContentBlocking::FFI::rust_content_blocker_create(
|
||||
rules_bytes.data(),
|
||||
rules_bytes.size());
|
||||
if (!engine)
|
||||
return Error::from_string_literal("Failed to create content blocker");
|
||||
|
||||
auto has_cosmetic_rules = rules_contain_cosmetic_rules(rules_bytes);
|
||||
|
||||
ContentBlocking::FFI::rust_content_blocker_free(m_engine);
|
||||
m_engine = engine;
|
||||
m_has_cosmetic_rules = has_cosmetic_rules;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ContentBlocker::is_filtered(URL::URL const& url) const
|
||||
{
|
||||
if (!filtering_enabled())
|
||||
return false;
|
||||
|
||||
if (url.scheme() == "data")
|
||||
return false;
|
||||
return contains(url.to_string());
|
||||
}
|
||||
|
||||
bool ContentBlocker::is_filtered(URL::URL const& url, URL::URL const& source_url, ResourceType resource_type) const
|
||||
{
|
||||
(void)source_url;
|
||||
(void)resource_type;
|
||||
return is_filtered(url);
|
||||
return is_filtered(url, url, ResourceType::Other);
|
||||
}
|
||||
|
||||
bool ContentBlocker::is_filtered(URL::URL const& url, URL::URL const& source_url, Optional<Fetch::Infrastructure::Request::Destination> const& destination, Optional<Fetch::Infrastructure::Request::InitiatorType> const& initiator_type, Fetch::Infrastructure::Request::Mode mode) const
|
||||
|
|
@ -48,90 +177,87 @@ bool ContentBlocker::is_filtered(URL::URL const& url, URL::URL const& source_url
|
|||
return is_filtered(url, source_url_for_matching(source_url), resource_type_from_fetch_metadata(destination, initiator_type, mode));
|
||||
}
|
||||
|
||||
bool ContentBlocker::contains(StringView text) const
|
||||
bool ContentBlocker::is_filtered(URL::URL const& url, URL::URL const& source_url, ResourceType resource_type) const
|
||||
{
|
||||
if (!m_matcher)
|
||||
return false;
|
||||
return m_matcher->contains(text);
|
||||
}
|
||||
|
||||
ErrorOr<void> ContentBlocker::set_patterns(ReadonlySpan<String> patterns)
|
||||
{
|
||||
Vector<String> network_patterns;
|
||||
m_cosmetic_rules.clear();
|
||||
|
||||
for (auto const& pattern : patterns) {
|
||||
auto pattern_view = pattern.bytes_as_string_view();
|
||||
auto cosmetic_marker = pattern_view.find("##"sv);
|
||||
if (!cosmetic_marker.has_value()) {
|
||||
network_patterns.append(pattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto selector = pattern_view.substring_view(cosmetic_marker.value() + 2);
|
||||
if (selector.is_empty())
|
||||
continue;
|
||||
|
||||
auto domains = pattern_view.substring_view(0, cosmetic_marker.value());
|
||||
if (domains.is_empty()) {
|
||||
CosmeticRule rule;
|
||||
rule.selector = TRY(String::from_utf8(selector));
|
||||
m_cosmetic_rules.append(move(rule));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto domain : domains.split_view(',')) {
|
||||
if (domain.is_empty())
|
||||
continue;
|
||||
CosmeticRule rule;
|
||||
rule.domain = TRY(String::from_utf8(domain));
|
||||
rule.selector = TRY(String::from_utf8(selector));
|
||||
m_cosmetic_rules.append(move(rule));
|
||||
}
|
||||
}
|
||||
|
||||
m_matcher = make<AsciiStringMatcher>(network_patterns);
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool cosmetic_rule_domain_matches(StringView domain, URL::URL const& url)
|
||||
{
|
||||
auto const& host = url.host();
|
||||
if (!host.has_value())
|
||||
if (!filtering_enabled() || !m_engine)
|
||||
return false;
|
||||
|
||||
auto host_string = host->serialize();
|
||||
auto host_view = host_string.bytes_as_string_view();
|
||||
if (host_view == domain)
|
||||
return true;
|
||||
|
||||
if (!host_view.ends_with(domain))
|
||||
return false;
|
||||
if (host_view.length() <= domain.length())
|
||||
if (url.scheme() == "data"sv)
|
||||
return false;
|
||||
|
||||
return host_view[host_view.length() - domain.length() - 1] == '.';
|
||||
}
|
||||
auto url_string = serialized_url_for_matching(url);
|
||||
auto normalized_source_url = source_url_for_matching(source_url);
|
||||
auto source_url_string = serialized_url_for_matching(normalized_source_url);
|
||||
auto request_type = resource_type_to_adblock_request_type(resource_type);
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_document(DOM::Document const& document) const
|
||||
{
|
||||
return cosmetic_style_sheet_for_url(document.fallback_base_url());
|
||||
return ContentBlocking::FFI::rust_content_blocker_matches(
|
||||
m_engine,
|
||||
reinterpret_cast<u8 const*>(url_string.characters()),
|
||||
url_string.length(),
|
||||
reinterpret_cast<u8 const*>(source_url_string.characters()),
|
||||
source_url_string.length(),
|
||||
reinterpret_cast<u8 const*>(request_type.characters_without_null_termination()),
|
||||
request_type.length());
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_url(URL::URL const& url) const
|
||||
{
|
||||
if (!filtering_enabled())
|
||||
return cosmetic_style_sheet_for_url(url, {}, {});
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_url(URL::URL const& url, ReadonlySpan<String> classes, ReadonlySpan<String> ids) const
|
||||
{
|
||||
if (!filtering_enabled() || !m_engine || !m_has_cosmetic_rules)
|
||||
return {};
|
||||
|
||||
StringBuilder builder;
|
||||
for (auto const& rule : m_cosmetic_rules) {
|
||||
if (rule.domain.has_value() && !cosmetic_rule_domain_matches(rule.domain->bytes_as_string_view(), url))
|
||||
continue;
|
||||
auto url_string = serialized_url(url);
|
||||
auto classes_string = join_lines(classes);
|
||||
if (classes_string.is_error())
|
||||
return {};
|
||||
|
||||
builder.append(rule.selector);
|
||||
builder.append(" { display: none !important; }\n"sv);
|
||||
}
|
||||
return builder.to_string_without_validation();
|
||||
auto ids_string = join_lines(ids);
|
||||
if (ids_string.is_error())
|
||||
return {};
|
||||
|
||||
auto classes_bytes = classes_string.value().bytes_as_string_view();
|
||||
auto ids_bytes = ids_string.value().bytes_as_string_view();
|
||||
|
||||
return take_rust_string(ContentBlocking::FFI::rust_content_blocker_cosmetic_css(
|
||||
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()));
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_document(DOM::Document const& document) const
|
||||
{
|
||||
if (!filtering_enabled() || !m_engine || !m_has_cosmetic_rules)
|
||||
return {};
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
return cosmetic_style_sheet_for_url(document.fallback_base_url(), classes, ids);
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -204,6 +330,7 @@ ContentBlocker::ResourceType ContentBlocker::resource_type_from_fetch_metadata(O
|
|||
case Request::InitiatorType::Script:
|
||||
return ResourceType::Script;
|
||||
case Request::InitiatorType::CSS:
|
||||
return ResourceType::Stylesheet;
|
||||
case Request::InitiatorType::EarlyHint:
|
||||
case Request::InitiatorType::Body:
|
||||
case Request::InitiatorType::Input:
|
||||
|
|
@ -229,138 +356,4 @@ URL::URL ContentBlocker::source_url_for_matching(URL::URL const& source_url)
|
|||
return parsed_url.release_value();
|
||||
}
|
||||
|
||||
AsciiStringMatcher::AsciiStringMatcher(ReadonlySpan<String> patterns)
|
||||
{
|
||||
struct BuildTimeNode {
|
||||
Vector<Transition> children;
|
||||
bool is_output { false };
|
||||
};
|
||||
|
||||
Vector<BuildTimeNode> build_time_nodes;
|
||||
build_time_nodes.append({});
|
||||
|
||||
for (u32 i = 0; i < patterns.size(); ++i) {
|
||||
auto const& pattern = patterns[i];
|
||||
u32 node = 0;
|
||||
for (u8 ch : pattern.bytes_as_string_view()) {
|
||||
VERIFY(is_ascii(ch));
|
||||
auto it = build_time_nodes[node].children.find_if(
|
||||
[ch](Transition const& t) { return t.character == ch; });
|
||||
|
||||
if (it != build_time_nodes[node].children.end()) {
|
||||
node = it->next_state;
|
||||
} else {
|
||||
u32 new_node = build_time_nodes.size();
|
||||
build_time_nodes.append({});
|
||||
build_time_nodes[node].children.empend(ch, new_node);
|
||||
node = new_node;
|
||||
}
|
||||
}
|
||||
|
||||
if (!build_time_nodes[node].is_output)
|
||||
build_time_nodes[node].is_output = true;
|
||||
}
|
||||
|
||||
Vector<u32> failure_links;
|
||||
failure_links.resize(build_time_nodes.size());
|
||||
|
||||
Queue<u32> queue;
|
||||
for (auto const& transition : build_time_nodes[0].children) {
|
||||
u32 child = transition.next_state;
|
||||
failure_links[child] = 0;
|
||||
queue.enqueue(child);
|
||||
}
|
||||
|
||||
while (!queue.is_empty()) {
|
||||
u32 current = queue.dequeue();
|
||||
for (auto& [character, child] : build_time_nodes[current].children) {
|
||||
u32 failure_link = failure_links[current];
|
||||
while (failure_link != 0) {
|
||||
auto it = build_time_nodes[failure_link].children.find_if(
|
||||
[character](Transition const& tr) { return tr.character == character; });
|
||||
if (it != build_time_nodes[failure_link].children.end()) {
|
||||
failure_link = it->next_state;
|
||||
break;
|
||||
}
|
||||
failure_link = failure_links[failure_link];
|
||||
}
|
||||
|
||||
u32 next_failure_link = failure_link;
|
||||
failure_links[child] = next_failure_link;
|
||||
|
||||
bool inherited = build_time_nodes[next_failure_link].is_output;
|
||||
if (inherited && !build_time_nodes[child].is_output)
|
||||
build_time_nodes[child].is_output = true;
|
||||
|
||||
queue.enqueue(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& node : build_time_nodes) {
|
||||
quick_sort(node.children, [](Transition const& a, Transition const& b) {
|
||||
return a.character < b.character;
|
||||
});
|
||||
}
|
||||
|
||||
m_nodes.resize(build_time_nodes.size());
|
||||
m_transitions.clear_with_capacity();
|
||||
|
||||
u32 transition_index = 0;
|
||||
for (u32 i = 0; i < build_time_nodes.size(); ++i) {
|
||||
auto& build_time_node = build_time_nodes[i];
|
||||
m_nodes[i].first_transition = transition_index;
|
||||
m_nodes[i].transition_count = build_time_node.children.size();
|
||||
m_nodes[i].output = build_time_node.is_output;
|
||||
m_transitions.extend(build_time_node.children);
|
||||
|
||||
transition_index += build_time_node.children.size();
|
||||
}
|
||||
}
|
||||
|
||||
bool AsciiStringMatcher::contains(StringView text) const
|
||||
{
|
||||
if (m_nodes.is_empty())
|
||||
return false;
|
||||
|
||||
auto get_children = [this](u32 state) -> ReadonlySpan<Transition> {
|
||||
return m_transitions.span().slice(m_nodes[state].first_transition, m_nodes[state].transition_count);
|
||||
};
|
||||
|
||||
u32 state = 0;
|
||||
for (u8 ch : text.bytes()) {
|
||||
auto const& children = get_children(state);
|
||||
|
||||
auto const* found = AK::binary_search(
|
||||
children,
|
||||
ch,
|
||||
nullptr,
|
||||
[](u8 needle, Transition const& transition) {
|
||||
if (needle > transition.character)
|
||||
return needle < transition.character ? -1 : 1;
|
||||
return needle < transition.character ? -1 : 0;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
state = 0;
|
||||
auto const& root_children = get_children(0);
|
||||
found = AK::binary_search(
|
||||
root_children,
|
||||
ch,
|
||||
nullptr,
|
||||
[](u8 needle, Transition const& transition) {
|
||||
if (needle > transition.character)
|
||||
return needle < transition.character ? -1 : 1;
|
||||
return needle < transition.character ? -1 : 0;
|
||||
});
|
||||
if (!found)
|
||||
continue;
|
||||
}
|
||||
|
||||
state = found->next_state;
|
||||
if (m_nodes[state].output)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
|
|
@ -18,30 +17,9 @@
|
|||
|
||||
namespace Web {
|
||||
|
||||
class AsciiStringMatcher {
|
||||
public:
|
||||
explicit AsciiStringMatcher(ReadonlySpan<String> patterns);
|
||||
|
||||
bool contains(StringView text) const;
|
||||
|
||||
private:
|
||||
struct Transition {
|
||||
u8 character { 0 };
|
||||
u32 next_state { 0 };
|
||||
};
|
||||
|
||||
struct Node {
|
||||
u32 first_transition { 0 };
|
||||
u8 transition_count { 0 };
|
||||
bool output { false };
|
||||
};
|
||||
|
||||
Vector<Node> m_nodes;
|
||||
Vector<Transition> m_transitions;
|
||||
};
|
||||
|
||||
class WEB_API ContentBlocker {
|
||||
AK_MAKE_NONCOPYABLE(ContentBlocker);
|
||||
AK_MAKE_NONMOVABLE(ContentBlocker);
|
||||
|
||||
public:
|
||||
enum class ResourceType : u8 {
|
||||
|
|
@ -61,6 +39,8 @@ public:
|
|||
|
||||
static ContentBlocker& the();
|
||||
|
||||
bool has_rules() const { return m_engine != nullptr; }
|
||||
bool has_cosmetic_rules() const { return m_has_cosmetic_rules; }
|
||||
bool filtering_enabled() const { return m_filtering_enabled; }
|
||||
void set_filtering_enabled(bool const enabled) { m_filtering_enabled = enabled; }
|
||||
|
||||
|
|
@ -68,10 +48,11 @@ public:
|
|||
bool is_filtered(URL::URL const&, URL::URL const& source_url, ResourceType) const;
|
||||
bool is_filtered(URL::URL const&, URL::URL const& source_url, Optional<Fetch::Infrastructure::Request::Destination> const&, Optional<Fetch::Infrastructure::Request::InitiatorType> const&, Fetch::Infrastructure::Request::Mode) const;
|
||||
ErrorOr<void> set_patterns(ReadonlySpan<String>);
|
||||
ErrorOr<void> set_rules_from_bytes(ReadonlyBytes);
|
||||
|
||||
bool has_cosmetic_rules() const { return !m_cosmetic_rules.is_empty(); }
|
||||
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;
|
||||
|
||||
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&);
|
||||
|
|
@ -80,16 +61,9 @@ private:
|
|||
ContentBlocker();
|
||||
~ContentBlocker();
|
||||
|
||||
bool contains(StringView text) const;
|
||||
|
||||
struct CosmeticRule {
|
||||
Optional<String> domain;
|
||||
String selector;
|
||||
};
|
||||
|
||||
bool m_filtering_enabled { true };
|
||||
OwnPtr<AsciiStringMatcher> m_matcher;
|
||||
Vector<CosmeticRule> m_cosmetic_rules;
|
||||
bool m_has_cosmetic_rules { false };
|
||||
void* m_engine { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1172,33 +1172,15 @@ void ConnectionFromClient::paste(u64 page_id, Utf16String text)
|
|||
page->page().focused_navigable().paste(text);
|
||||
}
|
||||
|
||||
static ErrorOr<Vector<String>> parse_content_blocker_patterns(Core::AnonymousBuffer const& patterns_buffer)
|
||||
{
|
||||
Vector<String> patterns;
|
||||
|
||||
for (auto line : StringView { patterns_buffer.bytes() }.split_view('\n', SplitBehavior::Nothing)) {
|
||||
if (line.ends_with('\r'))
|
||||
line = line.substring_view(0, line.length() - 1);
|
||||
if (line.is_empty())
|
||||
continue;
|
||||
|
||||
patterns.append(TRY(String::from_utf8(line)));
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
void ConnectionFromClient::set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns_buffer)
|
||||
{
|
||||
auto patterns_or_error = parse_content_blocker_patterns(patterns_buffer);
|
||||
if (patterns_or_error.is_error()) {
|
||||
dbgln("Failed to set content blockers: {}", patterns_or_error.error());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& blocker = Web::ContentBlocker::the();
|
||||
auto had_cosmetic_rules = blocker.has_cosmetic_rules();
|
||||
blocker.set_patterns(patterns_or_error.value()).release_value_but_fixme_should_propagate_errors();
|
||||
auto result = blocker.set_rules_from_bytes(patterns_buffer.bytes());
|
||||
if (result.is_error()) {
|
||||
dbgln("Failed to set content blockers: {}", result.error());
|
||||
return;
|
||||
}
|
||||
|
||||
if (had_cosmetic_rules || blocker.has_cosmetic_rules()) {
|
||||
if (auto page = this->page(page_id); page.has_value())
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ static ErrorOr<void> load_content_blockers(StringView config_path)
|
|||
continue;
|
||||
|
||||
auto pattern = TRY(String::from_utf8(line));
|
||||
TRY(patterns.try_append(move(pattern)));
|
||||
patterns.append(move(pattern));
|
||||
}
|
||||
|
||||
auto& content_blocker = Web::ContentBlocker::the();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Array.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/URL.h>
|
||||
|
|
@ -11,10 +12,11 @@
|
|||
|
||||
namespace Web {
|
||||
|
||||
static ContentBlocker& make_blocker(Vector<String> patterns)
|
||||
static ContentBlocker& make_blocker(Vector<String> rules)
|
||||
{
|
||||
auto& blocker = ContentBlocker::the();
|
||||
MUST(blocker.set_patterns(patterns));
|
||||
MUST(blocker.set_patterns(rules));
|
||||
blocker.set_filtering_enabled(true);
|
||||
return blocker;
|
||||
}
|
||||
|
||||
|
|
@ -35,95 +37,71 @@ TEST_CASE(empty_pattern_list)
|
|||
|
||||
TEST_CASE(basic_blocking)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
"ads."_string,
|
||||
"?banner"_string,
|
||||
"tracker"_string
|
||||
Vector<String> rules = {
|
||||
"||ads.example.com^"_string,
|
||||
"/banner.js"_string,
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto source_url = url("https://example.com/"sv);
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://example.com/ads.js"sv)));
|
||||
EXPECT(blocker.is_filtered(url("http://site.com/page.html?banner=true"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://tracker.example.org/ping"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("https://ds.example.com/page.html"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://ads.example.com/script.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
EXPECT(blocker.is_filtered(url("https://static.example.com/banner.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
EXPECT(!blocker.is_filtered(url("https://example.com/page.html"sv), source_url, ContentBlocker::ResourceType::Document));
|
||||
}
|
||||
|
||||
TEST_CASE(data_urls_exempt)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
Vector<String> rules = {
|
||||
{ "data:"_string },
|
||||
{ "evil.com"_string }
|
||||
{ "||evil.com^"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto source_url = url("https://example.com/"sv);
|
||||
|
||||
EXPECT(!blocker.is_filtered(url("data:text/plain,hello"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("data:image/png;base64,abc123"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://evil.com/script.js"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://evil.com/script.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
}
|
||||
|
||||
TEST_CASE(invalid_filter_bytes_keep_previous_rules)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "||ads.example.com^"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto source_url = url("https://example.com/"sv);
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://ads.example.com/script.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
|
||||
Array<u8, 1> invalid_utf8 { 0xff };
|
||||
auto result = blocker.set_rules_from_bytes(invalid_utf8.span());
|
||||
EXPECT(result.is_error());
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://ads.example.com/script.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
}
|
||||
|
||||
TEST_CASE(disable_filtering)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "example.com"_string },
|
||||
Vector<String> rules = {
|
||||
{ "||example.com^"_string },
|
||||
{ "##.ad"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
blocker.set_filtering_enabled(false);
|
||||
Vector<String> classes = { "ad"_string };
|
||||
Vector<String> ids;
|
||||
|
||||
EXPECT(!blocker.is_filtered(url("https://example.com"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("http://example.com/ads"sv)));
|
||||
EXPECT(blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv)).is_empty());
|
||||
EXPECT(blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv), classes.span(), ids.span()).is_empty());
|
||||
|
||||
blocker.set_filtering_enabled(true);
|
||||
EXPECT(blocker.is_filtered(url("https://example.com"sv)));
|
||||
EXPECT(!blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv)).is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE(substring_matches)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "ads"_string },
|
||||
{ "ad/"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://site.com/ads/banner.jpg"sv)));
|
||||
EXPECT(blocker.is_filtered(url("http://marketing.com/ad/page"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("https://site.com/content/article.html"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("http://advancedtech.com/home"sv)));
|
||||
}
|
||||
|
||||
TEST_CASE(file_scheme_can_be_filtered)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "secret"_string },
|
||||
{ ".txt"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
|
||||
EXPECT(blocker.is_filtered(url("file:///home/user/secret.txt"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("file:///home/user/document.pdf"sv)));
|
||||
}
|
||||
|
||||
TEST_CASE(query_parameters_and_fragments)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "#ad="_string },
|
||||
{ "?ad="_string },
|
||||
{ "#sponsored"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://site.com/page?ad=123"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://site.com/page#ad=456"sv)));
|
||||
EXPECT(blocker.is_filtered(url("https://site.com/page?ref=home&ad=1#sponsored"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("https://site.com/page?ref=home"sv)));
|
||||
EXPECT(!blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv), classes.span(), ids.span()).is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE(fetch_metadata_maps_to_resource_type)
|
||||
|
|
@ -145,60 +123,174 @@ TEST_CASE(fetch_metadata_maps_to_resource_type)
|
|||
EXPECT(resource_type({}, {}, Request::Mode::WebSocket) == ResourceType::WebSocket);
|
||||
}
|
||||
|
||||
TEST_CASE(blob_source_urls_are_normalized_for_matching)
|
||||
TEST_CASE(resource_type_options)
|
||||
{
|
||||
auto normalized = ContentBlocker::source_url_for_matching(url("blob:https://example.com/object-id"sv));
|
||||
EXPECT_EQ(normalized.to_string(), "https://example.com/object-id"sv);
|
||||
using Request = Fetch::Infrastructure::Request;
|
||||
|
||||
auto non_blob = ContentBlocker::source_url_for_matching(url("https://example.com/page"sv));
|
||||
EXPECT_EQ(non_blob.to_string(), "https://example.com/page"sv);
|
||||
}
|
||||
|
||||
TEST_CASE(contextual_filtering_uses_existing_matcher)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "blocked.js"_string }
|
||||
Vector<String> rules = {
|
||||
{ "||example.com/ad^$image"_string },
|
||||
{ "||example.com/font^$font"_string },
|
||||
{ "||example.com/document^$document"_string },
|
||||
{ "||example.com/fetch^$xmlhttprequest"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto source_url = url("https://example.com/"sv);
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://example.com/ad"sv), source_url, ContentBlocker::ResourceType::Image));
|
||||
EXPECT(!blocker.is_filtered(url("https://example.com/ad"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
EXPECT(blocker.is_filtered(
|
||||
url("https://tracker.example/blocked.js"sv),
|
||||
url("https://example.com/page"sv),
|
||||
Fetch::Infrastructure::Request::Destination::Script,
|
||||
Fetch::Infrastructure::Request::InitiatorType::CSS,
|
||||
Fetch::Infrastructure::Request::Mode::NoCORS));
|
||||
url("https://example.com/ad"sv),
|
||||
source_url,
|
||||
Request::Destination::Image,
|
||||
Request::InitiatorType::CSS,
|
||||
Request::Mode::NoCORS));
|
||||
EXPECT(blocker.is_filtered(
|
||||
url("https://example.com/font"sv),
|
||||
source_url,
|
||||
Request::Destination::Font,
|
||||
Request::InitiatorType::CSS,
|
||||
Request::Mode::NoCORS));
|
||||
EXPECT(blocker.is_filtered(
|
||||
url("https://example.com/document"sv),
|
||||
source_url,
|
||||
Request::Destination::Document,
|
||||
Optional<Request::InitiatorType> {},
|
||||
Request::Mode::Navigate));
|
||||
EXPECT(blocker.is_filtered(
|
||||
url("https://example.com/fetch"sv),
|
||||
source_url,
|
||||
Optional<Request::Destination> {},
|
||||
Request::InitiatorType::Fetch,
|
||||
Request::Mode::CORS));
|
||||
}
|
||||
|
||||
TEST_CASE(cosmetic_rules_generate_user_css)
|
||||
TEST_CASE(file_scheme_fallback)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "blocked.js"_string },
|
||||
{ "##.ad"_string },
|
||||
{ "example.com##.sponsored"_string },
|
||||
{ "other.example##.other"_string }
|
||||
Vector<String> rules = {
|
||||
{ "secret"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
|
||||
EXPECT(blocker.has_cosmetic_rules());
|
||||
EXPECT(blocker.is_filtered(url("https://tracker.example/blocked.js"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("https://tracker.example/##.ad"sv)));
|
||||
|
||||
auto style_sheet = blocker.cosmetic_style_sheet_for_url(url("https://www.example.com/page"sv));
|
||||
EXPECT(style_sheet.contains(".ad { display: none !important; }"sv));
|
||||
EXPECT(style_sheet.contains(".sponsored { display: none !important; }"sv));
|
||||
EXPECT(!style_sheet.contains(".other { display: none !important; }"sv));
|
||||
EXPECT(blocker.is_filtered(url("file:///home/user/secret.txt"sv)));
|
||||
EXPECT(!blocker.is_filtered(url("file:///home/user/public.txt"sv)));
|
||||
}
|
||||
|
||||
TEST_CASE(clearing_patterns_clears_cosmetic_rules)
|
||||
TEST_CASE(third_party_option)
|
||||
{
|
||||
auto& blocker = make_blocker({ "##.ad"_string });
|
||||
EXPECT(blocker.has_cosmetic_rules());
|
||||
Vector<String> rules = {
|
||||
{ "||tracker.example^$third-party"_string },
|
||||
};
|
||||
|
||||
MUST(blocker.set_patterns({}));
|
||||
EXPECT(!blocker.has_cosmetic_rules());
|
||||
EXPECT(blocker.cosmetic_style_sheet_for_url(url("https://example.com/"sv)).is_empty());
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://tracker.example/pixel.gif"sv), url("https://site.example/"sv), ContentBlocker::ResourceType::Image));
|
||||
EXPECT(!blocker.is_filtered(url("https://tracker.example/pixel.gif"sv), url("https://www.tracker.example/"sv), ContentBlocker::ResourceType::Image));
|
||||
}
|
||||
|
||||
TEST_CASE(blob_source_url_uses_embedded_url)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "||tracker.example/third-party.gif$third-party"_string },
|
||||
{ "||tracker.example/first-party.gif$first-party"_string },
|
||||
{ "||ads.example/domain.js$domain=www.tracker.example"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto same_site_blob_source = url("blob:https://www.tracker.example/4dd3d7ea-8bd7-4fe0-a121-79c18e2be4b2"sv);
|
||||
auto cross_site_blob_source = url("blob:https://site.example/4dd3d7ea-8bd7-4fe0-a121-79c18e2be4b2"sv);
|
||||
|
||||
EXPECT(!blocker.is_filtered(url("https://tracker.example/third-party.gif"sv), same_site_blob_source, ContentBlocker::ResourceType::Image));
|
||||
EXPECT(blocker.is_filtered(url("https://tracker.example/first-party.gif"sv), same_site_blob_source, ContentBlocker::ResourceType::Image));
|
||||
EXPECT(blocker.is_filtered(url("https://tracker.example/third-party.gif"sv), cross_site_blob_source, ContentBlocker::ResourceType::Image));
|
||||
EXPECT(!blocker.is_filtered(url("https://tracker.example/first-party.gif"sv), cross_site_blob_source, ContentBlocker::ResourceType::Image));
|
||||
EXPECT(blocker.is_filtered(url("https://ads.example/domain.js"sv), same_site_blob_source, ContentBlocker::ResourceType::Script));
|
||||
EXPECT(!blocker.is_filtered(url("https://ads.example/domain.js"sv), cross_site_blob_source, ContentBlocker::ResourceType::Script));
|
||||
}
|
||||
|
||||
TEST_CASE(document_navigation_is_first_party_to_itself)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "||localhost^$third-party,document"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto target_url = url("http://localhost:1234/content-blocker-target"sv);
|
||||
|
||||
EXPECT(blocker.is_filtered(target_url, url("https://source.example/"sv), ContentBlocker::ResourceType::Document));
|
||||
EXPECT(!blocker.is_filtered(target_url, target_url, ContentBlocker::ResourceType::Document));
|
||||
}
|
||||
|
||||
TEST_CASE(exception_rules)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "||ads.example.com^"_string },
|
||||
{ "@@||ads.example.com/allowed.js"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
auto source_url = url("https://example.com/"sv);
|
||||
|
||||
EXPECT(blocker.is_filtered(url("https://ads.example.com/blocked.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
EXPECT(!blocker.is_filtered(url("https://ads.example.com/allowed.js"sv), source_url, ContentBlocker::ResourceType::Script));
|
||||
}
|
||||
|
||||
TEST_CASE(cosmetic_style_sheet)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "example.com##.ad-banner"_string },
|
||||
{ "example.com###sponsor"_string },
|
||||
{ "example.com##.styled-sponsor:style(visibility: hidden)"_string },
|
||||
{ "##.generic-ad"_string },
|
||||
{ "###generic-sponsor"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
Vector<String> classes = { "generic-ad"_string };
|
||||
Vector<String> ids = { "generic-sponsor"_string };
|
||||
|
||||
auto style_sheet = blocker.cosmetic_style_sheet_for_url(url("https://example.com/"sv), classes, ids);
|
||||
|
||||
EXPECT(style_sheet.contains(".ad-banner { display: none !important; }"sv));
|
||||
EXPECT(style_sheet.contains("#sponsor { display: none !important; }"sv));
|
||||
EXPECT(style_sheet.contains(".styled-sponsor { visibility: hidden; }"sv));
|
||||
EXPECT(style_sheet.contains(".generic-ad { display: none !important; }"sv));
|
||||
EXPECT(style_sheet.contains("#generic-sponsor { display: none !important; }"sv));
|
||||
}
|
||||
|
||||
TEST_CASE(generic_cosmetic_selector_lists_match_later_selectors)
|
||||
{
|
||||
Vector<String> rules = {
|
||||
{ "##.first-ad-class, .second-ad-class"_string },
|
||||
{ "##.first-ad-id, #second-ad-id"_string },
|
||||
{ "##.class-keyed-arm, [data-ad]"_string },
|
||||
{ "##.class-keyed-arm, div.sponsor"_string },
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(rules));
|
||||
Vector<String> classes = { "second-ad-class"_string };
|
||||
Vector<String> ids = { "second-ad-id"_string };
|
||||
Vector<String> no_classes;
|
||||
Vector<String> no_ids;
|
||||
|
||||
auto style_sheet = blocker.cosmetic_style_sheet_for_url(url("https://example.com/"sv), classes, ids);
|
||||
auto style_sheet_without_class_or_id_hints = blocker.cosmetic_style_sheet_for_url(url("https://example.com/"sv), no_classes, no_ids);
|
||||
|
||||
EXPECT(style_sheet.contains(".first-ad-class, .second-ad-class { display: none !important; }"sv));
|
||||
EXPECT(style_sheet.contains(".first-ad-id, #second-ad-id { display: none !important; }"sv));
|
||||
EXPECT(style_sheet_without_class_or_id_hints.contains(".class-keyed-arm, [data-ad] { display: none !important; }"sv));
|
||||
EXPECT(style_sheet_without_class_or_id_hints.contains(".class-keyed-arm, div.sponsor { display: none !important; }"sv));
|
||||
}
|
||||
|
||||
TEST_CASE(cosmetic_rule_detection)
|
||||
{
|
||||
auto& blocker_without_cosmetics = make_blocker({ "||example.com/ad^"_string });
|
||||
EXPECT(!blocker_without_cosmetics.has_cosmetic_rules());
|
||||
EXPECT(blocker_without_cosmetics.cosmetic_style_sheet_for_url(url("https://example.com/"sv), {}, {}).is_empty());
|
||||
|
||||
auto& blocker_with_cosmetics = make_blocker({ "example.com##.ad-banner"_string });
|
||||
EXPECT(blocker_with_cosmetics.has_cosmetic_rules());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ static ErrorOr<void> load_content_blockers()
|
|||
continue;
|
||||
|
||||
auto pattern = TRY(String::from_utf8(line));
|
||||
TRY(patterns.try_append(move(pattern)));
|
||||
patterns.append(move(pattern));
|
||||
}
|
||||
|
||||
auto& content_blocker = Web::ContentBlocker::the();
|
||||
|
|
|
|||
Loading…
Reference in a new issue