2026-05-21 11:36:25 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
#include <AK/Array.h>
|
2026-05-21 11:36:25 -03:00
|
|
|
#include <LibTest/TestCase.h>
|
|
|
|
|
#include <LibURL/Parser.h>
|
|
|
|
|
#include <LibURL/URL.h>
|
|
|
|
|
#include <LibWeb/Loader/ContentBlocker.h>
|
|
|
|
|
|
|
|
|
|
namespace Web {
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
static ContentBlocker& make_blocker(Vector<String> rules)
|
2026-05-21 11:36:25 -03:00
|
|
|
{
|
|
|
|
|
auto& blocker = ContentBlocker::the();
|
2026-05-23 17:15:37 -03:00
|
|
|
MUST(blocker.set_patterns(rules));
|
|
|
|
|
blocker.set_filtering_enabled(true);
|
2026-05-21 11:36:25 -03:00
|
|
|
return blocker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static URL::URL url(StringView string)
|
|
|
|
|
{
|
|
|
|
|
auto result = URL::Parser::basic_parse(string);
|
|
|
|
|
EXPECT(result.has_value());
|
|
|
|
|
return result.release_value();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(empty_pattern_list)
|
|
|
|
|
{
|
|
|
|
|
auto& blocker = make_blocker({});
|
|
|
|
|
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("https://anything.com"sv)));
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("data:text/plain,hi"sv)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(basic_blocking)
|
|
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
|
|
|
|
"||ads.example.com^"_string,
|
|
|
|
|
"/banner.js"_string,
|
2026-05-21 11:36:25 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
|
|
|
|
auto source_url = url("https://example.com/"sv);
|
2026-05-21 11:36:25 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(data_urls_exempt)
|
|
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
2026-05-21 11:36:25 -03:00
|
|
|
{ "data:"_string },
|
2026-05-23 17:15:37 -03:00
|
|
|
{ "||evil.com^"_string }
|
2026-05-21 11:36:25 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
|
|
|
|
auto source_url = url("https://example.com/"sv);
|
2026-05-21 11:36:25 -03:00
|
|
|
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("data:text/plain,hello"sv)));
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("data:image/png;base64,abc123"sv)));
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(disable_filtering)
|
|
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
|
|
|
|
{ "||example.com^"_string },
|
2026-05-21 13:35:29 -03:00
|
|
|
{ "##.ad"_string }
|
2026-05-21 11:36:25 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
2026-05-21 11:36:25 -03:00
|
|
|
blocker.set_filtering_enabled(false);
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> classes = { "ad"_string };
|
|
|
|
|
Vector<String> ids;
|
2026-05-21 11:36:25 -03:00
|
|
|
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("https://example.com"sv)));
|
|
|
|
|
EXPECT(!blocker.is_filtered(url("http://example.com/ads"sv)));
|
2026-05-23 17:15:37 -03:00
|
|
|
EXPECT(blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv), classes.span(), ids.span()).is_empty());
|
2026-05-21 11:36:25 -03:00
|
|
|
|
|
|
|
|
blocker.set_filtering_enabled(true);
|
|
|
|
|
EXPECT(blocker.is_filtered(url("https://example.com"sv)));
|
2026-05-23 17:15:37 -03:00
|
|
|
EXPECT(!blocker.cosmetic_style_sheet_for_url(url("https://example.com"sv), classes.span(), ids.span()).is_empty());
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(fetch_metadata_maps_to_resource_type)
|
2026-05-21 11:36:25 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
using Request = Fetch::Infrastructure::Request;
|
|
|
|
|
using ResourceType = ContentBlocker::ResourceType;
|
|
|
|
|
|
|
|
|
|
auto resource_type = [](Optional<Request::Destination> destination, Optional<Request::InitiatorType> initiator_type, Request::Mode mode = Request::Mode::NoCORS) {
|
|
|
|
|
return ContentBlocker::resource_type_from_fetch_metadata(destination, initiator_type, mode);
|
2026-05-21 11:36:25 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
EXPECT(resource_type(Request::Destination::Document, {}) == ResourceType::Document);
|
|
|
|
|
EXPECT(resource_type(Request::Destination::Font, Request::InitiatorType::CSS) == ResourceType::Font);
|
|
|
|
|
EXPECT(resource_type(Request::Destination::Image, Request::InitiatorType::CSS) == ResourceType::Image);
|
|
|
|
|
EXPECT(resource_type(Request::Destination::Style, {}) == ResourceType::Stylesheet);
|
|
|
|
|
EXPECT(resource_type(Request::Destination::IFrame, {}) == ResourceType::Subdocument);
|
|
|
|
|
EXPECT(resource_type({}, Request::InitiatorType::Fetch) == ResourceType::XMLHttpRequest);
|
|
|
|
|
EXPECT(resource_type({}, Request::InitiatorType::Ping) == ResourceType::Ping);
|
|
|
|
|
EXPECT(resource_type({}, {}, Request::Mode::WebSocket) == ResourceType::WebSocket);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(resource_type_options)
|
|
|
|
|
{
|
|
|
|
|
using Request = Fetch::Infrastructure::Request;
|
|
|
|
|
|
|
|
|
|
Vector<String> rules = {
|
|
|
|
|
{ "||example.com/ad^$image"_string },
|
|
|
|
|
{ "||example.com/font^$font"_string },
|
|
|
|
|
{ "||example.com/document^$document"_string },
|
|
|
|
|
{ "||example.com/fetch^$xmlhttprequest"_string },
|
|
|
|
|
};
|
2026-05-21 11:36:25 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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://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));
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(file_scheme_fallback)
|
2026-05-21 11:36:25 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
2026-05-21 11:36:25 -03:00
|
|
|
{ "secret"_string },
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
2026-05-21 11:36:25 -03:00
|
|
|
|
|
|
|
|
EXPECT(blocker.is_filtered(url("file:///home/user/secret.txt"sv)));
|
2026-05-23 17:15:37 -03:00
|
|
|
EXPECT(!blocker.is_filtered(url("file:///home/user/public.txt"sv)));
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(third_party_option)
|
2026-05-21 11:36:25 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
|
|
|
|
{ "||tracker.example^$third-party"_string },
|
2026-05-21 11:36:25 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
2026-05-21 11:36:25 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(blob_source_url_uses_embedded_url)
|
2026-05-21 11:44:02 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
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 },
|
|
|
|
|
};
|
2026-05-21 11:44:02 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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 },
|
2026-05-21 11:44:02 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:44:02 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(exception_rules)
|
2026-05-21 11:44:02 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
Vector<String> rules = {
|
|
|
|
|
{ "||ads.example.com^"_string },
|
|
|
|
|
{ "@@||ads.example.com/allowed.js"_string },
|
|
|
|
|
};
|
2026-05-21 11:44:02 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:44:02 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(cosmetic_style_sheet)
|
2026-05-21 11:44:02 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
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 },
|
2026-05-21 11:44:02 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker = make_blocker(move(rules));
|
|
|
|
|
Vector<String> classes = { "generic-ad"_string };
|
|
|
|
|
Vector<String> ids = { "generic-sponsor"_string };
|
2026-05-21 11:44:02 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 11:44:02 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(generic_cosmetic_selector_lists_match_later_selectors)
|
2026-05-21 13:35:29 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
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 },
|
2026-05-21 13:35:29 -03:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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;
|
2026-05-21 13:35:29 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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);
|
2026-05-21 13:35:29 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
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));
|
2026-05-21 13:35:29 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
TEST_CASE(cosmetic_rule_detection)
|
2026-05-21 13:35:29 -03:00
|
|
|
{
|
2026-05-23 17:15:37 -03:00
|
|
|
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());
|
2026-05-21 13:35:29 -03:00
|
|
|
|
2026-05-23 17:15:37 -03:00
|
|
|
auto& blocker_with_cosmetics = make_blocker({ "example.com##.ad-banner"_string });
|
|
|
|
|
EXPECT(blocker_with_cosmetics.has_cosmetic_rules());
|
2026-05-21 13:35:29 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-21 11:36:25 -03:00
|
|
|
}
|