LibWeb: Add cosmetic rules to ContentBlocker
Split cosmetic blocker rules out from network patterns. Expose matching rules as user CSS through StyleScope. Invalidate affected user style caches when blocker state changes. Generated cosmetic CSS now respects disabled content blocking.
This commit is contained in:
parent
43b8a8b099
commit
c974e616c0
7 changed files with 184 additions and 10 deletions
|
|
@ -5,6 +5,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/ReportTime.h>
|
||||
#include <LibWeb/CSS/CSSConditionRule.h>
|
||||
#include <LibWeb/CSS/CSSContainerRule.h>
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
#include <LibWeb/CSS/StyleScope.h>
|
||||
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/Loader/ContentBlocker.h>
|
||||
#include <LibWeb/Namespace.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
|
||||
|
|
@ -122,7 +124,7 @@ void StyleScope::build_rule_cache()
|
|||
constructed_style_sheet = style_sheet;
|
||||
});
|
||||
|
||||
if (constructed_style_sheet && !saw_more_than_one_style_sheet && constructed_style_sheet->constructed() && !document().page().user_style().has_value()) {
|
||||
if (constructed_style_sheet && !saw_more_than_one_style_sheet && constructed_style_sheet->constructed() && !document().page().user_style().has_value() && !ContentBlocker::the().has_cosmetic_rules()) {
|
||||
m_rule_cache = constructed_style_sheet->shared_single_constructed_sheet_style_cache(*this);
|
||||
return;
|
||||
}
|
||||
|
|
@ -167,8 +169,21 @@ void StyleScope::build_user_style_sheet_if_needed()
|
|||
if (m_user_style_sheet)
|
||||
return;
|
||||
|
||||
if (auto user_style_source = document().page().user_style(); user_style_source.has_value())
|
||||
m_user_style_sheet = GC::make_root(parse_css_stylesheet(CSS::Parser::ParsingParams(document()), user_style_source.value()));
|
||||
auto user_style_source = document().page().user_style();
|
||||
auto content_blocker_style_source = ContentBlocker::the().cosmetic_style_sheet_for_document(document());
|
||||
if (!user_style_source.has_value() && content_blocker_style_source.is_empty())
|
||||
return;
|
||||
|
||||
StringBuilder source;
|
||||
if (user_style_source.has_value())
|
||||
source.append(user_style_source.value());
|
||||
if (!content_blocker_style_source.is_empty()) {
|
||||
if (!source.is_empty())
|
||||
source.append('\n');
|
||||
source.append(content_blocker_style_source);
|
||||
}
|
||||
|
||||
m_user_style_sheet = GC::make_root(parse_css_stylesheet(CSS::Parser::ParsingParams(document()), source.to_string_without_validation()));
|
||||
}
|
||||
|
||||
void StyleScope::build_rule_cache_if_needed() const
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
#include <AK/Queue.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/Span.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/Loader/ContentBlocker.h>
|
||||
|
||||
namespace Web {
|
||||
|
|
@ -55,10 +57,83 @@ bool ContentBlocker::contains(StringView text) const
|
|||
|
||||
ErrorOr<void> ContentBlocker::set_patterns(ReadonlySpan<String> patterns)
|
||||
{
|
||||
m_matcher = make<AsciiStringMatcher>(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())
|
||||
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())
|
||||
return false;
|
||||
|
||||
return host_view[host_view.length() - domain.length() - 1] == '.';
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_document(DOM::Document const& document) const
|
||||
{
|
||||
return cosmetic_style_sheet_for_url(document.fallback_base_url());
|
||||
}
|
||||
|
||||
String ContentBlocker::cosmetic_style_sheet_for_url(URL::URL const& url) const
|
||||
{
|
||||
if (!filtering_enabled())
|
||||
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;
|
||||
|
||||
builder.append(rule.selector);
|
||||
builder.append(" { display: none !important; }\n"sv);
|
||||
}
|
||||
return builder.to_string_without_validation();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
using Fetch::Infrastructure::Request;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
|
|
@ -68,6 +69,10 @@ public:
|
|||
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>);
|
||||
|
||||
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;
|
||||
|
||||
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&);
|
||||
|
||||
|
|
@ -77,8 +82,14 @@ private:
|
|||
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
#include <LibWeb/HTML/SelectedFile.h>
|
||||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Loader/ContentBlocker.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
|
@ -758,12 +759,41 @@ GC::Ptr<HTML::HTMLMediaElement> Page::media_context_menu_element()
|
|||
void Page::set_user_style(String source)
|
||||
{
|
||||
m_user_style_sheet_source = source;
|
||||
if (top_level_traversable_is_initialized() && top_level_traversable()->active_document()) {
|
||||
auto& document = *top_level_traversable()->active_document();
|
||||
invalidate_user_style();
|
||||
}
|
||||
|
||||
void Page::set_content_blocking_enabled(bool enabled)
|
||||
{
|
||||
auto& blocker = ContentBlocker::the();
|
||||
if (blocker.filtering_enabled() == enabled)
|
||||
return;
|
||||
|
||||
auto has_cosmetic_rules = blocker.has_cosmetic_rules();
|
||||
blocker.set_filtering_enabled(enabled);
|
||||
if (has_cosmetic_rules)
|
||||
invalidate_user_style();
|
||||
}
|
||||
|
||||
void Page::invalidate_user_style()
|
||||
{
|
||||
if (!top_level_traversable_is_initialized() || !top_level_traversable()->active_document())
|
||||
return;
|
||||
|
||||
auto invalidate_document = [](DOM::Document& document) {
|
||||
document.style_scope().invalidate_rule_cache();
|
||||
document.for_each_shadow_root([](auto& shadow_root) {
|
||||
shadow_root.style_scope().invalidate_rule_cache();
|
||||
shadow_root.invalidate_style(DOM::StyleInvalidationReason::StyleSheetReplace);
|
||||
});
|
||||
document.invalidate_style(DOM::StyleInvalidationReason::StyleSheetReplace);
|
||||
};
|
||||
|
||||
auto& active_document = *top_level_traversable()->active_document();
|
||||
invalidate_document(active_document);
|
||||
|
||||
for (auto& navigable : active_document.descendant_navigables()) {
|
||||
if (auto document = navigable->active_document())
|
||||
invalidate_document(*document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,8 @@ public:
|
|||
|
||||
Optional<String> const& user_style() const { return m_user_style_sheet_source; }
|
||||
void set_user_style(String source);
|
||||
void set_content_blocking_enabled(bool);
|
||||
void invalidate_user_style();
|
||||
|
||||
bool pdf_viewer_supported() const { return m_pdf_viewer_supported; }
|
||||
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ void ConnectionFromClient::debug_request(u64 page_id, ByteString request, ByteSt
|
|||
}
|
||||
|
||||
if (request == "content-blocking") {
|
||||
Web::ContentBlocker::the().set_filtering_enabled(argument == "on");
|
||||
page->page().set_content_blocking_enabled(argument == "on");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1144,9 +1144,16 @@ void ConnectionFromClient::paste(u64 page_id, Utf16String text)
|
|||
page->page().focused_navigable().paste(text);
|
||||
}
|
||||
|
||||
void ConnectionFromClient::set_content_blockers(u64, Vector<String> patterns)
|
||||
void ConnectionFromClient::set_content_blockers(u64 page_id, Vector<String> patterns)
|
||||
{
|
||||
Web::ContentBlocker::the().set_patterns(patterns).release_value_but_fixme_should_propagate_errors();
|
||||
auto& blocker = Web::ContentBlocker::the();
|
||||
auto had_cosmetic_rules = blocker.has_cosmetic_rules();
|
||||
blocker.set_patterns(patterns).release_value_but_fixme_should_propagate_errors();
|
||||
|
||||
if (had_cosmetic_rules || blocker.has_cosmetic_rules()) {
|
||||
if (auto page = this->page(page_id); page.has_value())
|
||||
page->page().invalidate_user_style();
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionFromClient::set_autoplay_allowed_on_all_websites(u64)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ TEST_CASE(data_urls_exempt)
|
|||
TEST_CASE(disable_filtering)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "example.com"_string }
|
||||
{ "example.com"_string },
|
||||
{ "##.ad"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
|
|
@ -74,9 +75,11 @@ TEST_CASE(disable_filtering)
|
|||
|
||||
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());
|
||||
|
||||
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)
|
||||
|
|
@ -167,4 +170,35 @@ TEST_CASE(contextual_filtering_uses_existing_matcher)
|
|||
Fetch::Infrastructure::Request::Mode::NoCORS));
|
||||
}
|
||||
|
||||
TEST_CASE(cosmetic_rules_generate_user_css)
|
||||
{
|
||||
Vector<String> patterns = {
|
||||
{ "blocked.js"_string },
|
||||
{ "##.ad"_string },
|
||||
{ "example.com##.sponsored"_string },
|
||||
{ "other.example##.other"_string }
|
||||
};
|
||||
|
||||
auto& blocker = make_blocker(move(patterns));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
TEST_CASE(clearing_patterns_clears_cosmetic_rules)
|
||||
{
|
||||
auto& blocker = make_blocker({ "##.ad"_string });
|
||||
EXPECT(blocker.has_cosmetic_rules());
|
||||
|
||||
MUST(blocker.set_patterns({}));
|
||||
EXPECT(!blocker.has_cosmetic_rules());
|
||||
EXPECT(blocker.cosmetic_style_sheet_for_url(url("https://example.com/"sv)).is_empty());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue