/* * Copyright (c) 2025, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include namespace Web { static ContentBlocker& make_blocker(Vector rules) { auto& blocker = ContentBlocker::the(); MUST(blocker.set_patterns(rules)); blocker.set_filtering_enabled(true); 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) { Vector rules = { "||ads.example.com^"_string, "/banner.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/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 rules = { { "data:"_string }, { "||evil.com^"_string } }; 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), source_url, ContentBlocker::ResourceType::Script)); } TEST_CASE(invalid_filter_bytes_keep_previous_rules) { Vector 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 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 rules = { { "||example.com^"_string }, { "##.ad"_string } }; auto& blocker = make_blocker(move(rules)); blocker.set_filtering_enabled(false); Vector classes = { "ad"_string }; Vector 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), 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), classes.span(), ids.span()).is_empty()); } TEST_CASE(fetch_metadata_maps_to_resource_type) { using Request = Fetch::Infrastructure::Request; using ResourceType = ContentBlocker::ResourceType; auto resource_type = [](Optional destination, Optional initiator_type, Request::Mode mode = Request::Mode::NoCORS) { return ContentBlocker::resource_type_from_fetch_metadata(destination, initiator_type, mode); }; 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 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(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::Mode::Navigate)); EXPECT(blocker.is_filtered( url("https://example.com/fetch"sv), source_url, Optional {}, Request::InitiatorType::Fetch, Request::Mode::CORS)); } TEST_CASE(file_scheme_fallback) { Vector rules = { { "secret"_string }, }; auto& blocker = make_blocker(move(rules)); EXPECT(blocker.is_filtered(url("file:///home/user/secret.txt"sv))); EXPECT(!blocker.is_filtered(url("file:///home/user/public.txt"sv))); } TEST_CASE(third_party_option) { Vector rules = { { "||tracker.example^$third-party"_string }, }; 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 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 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 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 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 classes = { "generic-ad"_string }; Vector 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 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 classes = { "second-ad-class"_string }; Vector ids = { "second-ad-id"_string }; Vector no_classes; Vector 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()); } }