Rewild Your Web
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

ui: content blocker panel

Signed-off-by: webbeef <me@webbeef.org>

webbeef 316b60cc f0f39709

+1170 -148
+21 -70
Cargo.lock
··· 756 756 "log", 757 757 "mime_guess", 758 758 "parking_lot", 759 + "rusqlite", 759 760 "rustls", 760 761 "serde", 761 762 "serde_json", ··· 1952 1953 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" 1953 1954 dependencies = [ 1954 1955 "data-encoding", 1955 - "syn 2.0.117", 1956 + "syn 1.0.109", 1956 1957 ] 1957 1958 1958 1959 [[package]] ··· 4473 4474 "icu_properties 1.5.1", 4474 4475 "icu_provider 1.5.0", 4475 4476 "icu_provider_adapters", 4476 - "icu_segmenter 1.5.0", 4477 + "icu_segmenter", 4477 4478 "icu_timezone", 4478 4479 "tinystr 0.7.6", 4479 4480 "unicode-bidi", ··· 4659 4660 checksum = "52b1a7fbdbf3958f1be8354cb59ac73f165b7b7082d447ff2090355c9a069120" 4660 4661 4661 4662 [[package]] 4662 - name = "icu_locale" 4663 - version = "2.2.0" 4664 - source = "registry+https://github.com/rust-lang/crates.io-index" 4665 - checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" 4666 - dependencies = [ 4667 - "icu_collections 2.2.0", 4668 - "icu_locale_core", 4669 - "icu_locale_data", 4670 - "icu_provider 2.2.0", 4671 - "potential_utf", 4672 - "tinystr 0.8.3", 4673 - "zerovec 0.11.6", 4674 - ] 4675 - 4676 - [[package]] 4677 4663 name = "icu_locale_core" 4678 4664 version = "2.2.0" 4679 4665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4681 4667 dependencies = [ 4682 4668 "displaydoc", 4683 4669 "litemap 0.8.2", 4684 - "serde", 4685 4670 "tinystr 0.8.3", 4686 4671 "writeable 0.6.3", 4687 4672 "zerovec 0.11.6", 4688 4673 ] 4689 4674 4690 4675 [[package]] 4691 - name = "icu_locale_data" 4692 - version = "2.2.0" 4693 - source = "registry+https://github.com/rust-lang/crates.io-index" 4694 - checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" 4695 - 4696 - [[package]] 4697 4676 name = "icu_locid" 4698 4677 version = "1.5.0" 4699 4678 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4870 4849 dependencies = [ 4871 4850 "displaydoc", 4872 4851 "icu_locale_core", 4873 - "serde", 4874 - "stable_deref_trait", 4875 4852 "writeable 0.6.3", 4876 4853 "yoke 0.8.2", 4877 4854 "zerofrom", ··· 4914 4891 "icu_collections 1.5.0", 4915 4892 "icu_locid", 4916 4893 "icu_provider 1.5.0", 4917 - "icu_segmenter_data 1.5.1", 4894 + "icu_segmenter_data", 4918 4895 "utf8_iter", 4919 4896 "zerovec 0.10.4", 4920 4897 ] 4921 4898 4922 4899 [[package]] 4923 - name = "icu_segmenter" 4924 - version = "2.2.0" 4925 - source = "registry+https://github.com/rust-lang/crates.io-index" 4926 - checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" 4927 - dependencies = [ 4928 - "core_maths", 4929 - "icu_collections 2.2.0", 4930 - "icu_locale", 4931 - "icu_provider 2.2.0", 4932 - "icu_segmenter_data 2.2.0", 4933 - "potential_utf", 4934 - "utf8_iter", 4935 - "zerovec 0.11.6", 4936 - ] 4937 - 4938 - [[package]] 4939 4900 name = "icu_segmenter_data" 4940 4901 version = "1.5.1" 4941 4902 source = "registry+https://github.com/rust-lang/crates.io-index" 4942 4903 checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb" 4943 - 4944 - [[package]] 4945 - name = "icu_segmenter_data" 4946 - version = "2.2.0" 4947 - source = "registry+https://github.com/rust-lang/crates.io-index" 4948 - checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" 4949 4904 4950 4905 [[package]] 4951 4906 name = "icu_timezone" ··· 4997 4952 4998 4953 [[package]] 4999 4954 name = "idna_adapter" 5000 - version = "1.2.1" 4955 + version = "1.2.2" 5001 4956 source = "registry+https://github.com/rust-lang/crates.io-index" 5002 - checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 4957 + checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" 5003 4958 dependencies = [ 5004 4959 "icu_normalizer 2.2.0", 5005 4960 "icu_properties 2.2.0", ··· 7766 7721 source = "registry+https://github.com/rust-lang/crates.io-index" 7767 7722 checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" 7768 7723 dependencies = [ 7769 - "serde_core", 7770 - "writeable 0.6.3", 7771 7724 "zerovec 0.11.6", 7772 7725 ] 7773 7726 ··· 8887 8840 [[package]] 8888 8841 name = "selectors" 8889 8842 version = "0.37.0" 8890 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 8843 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 8891 8844 dependencies = [ 8892 8845 "bitflags 2.11.1", 8893 8846 "cssparser", ··· 9589 9542 "euclid", 9590 9543 "icu_locid", 9591 9544 "icu_properties 1.5.1", 9592 - "icu_segmenter 1.5.0", 9545 + "icu_segmenter", 9593 9546 "itertools 0.14.0", 9594 9547 "kurbo 0.12.0", 9595 9548 "log", ··· 10568 10521 [[package]] 10569 10522 name = "servo_arc" 10570 10523 version = "0.4.3" 10571 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 10524 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 10572 10525 dependencies = [ 10573 10526 "serde", 10574 10527 "stable_deref_trait", ··· 11053 11006 [[package]] 11054 11007 name = "stylo" 11055 11008 version = "0.16.0" 11056 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11009 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11057 11010 dependencies = [ 11058 11011 "app_units", 11059 11012 "arrayvec", ··· 11064 11017 "derive_more", 11065 11018 "encoding_rs", 11066 11019 "euclid", 11067 - "icu_segmenter 2.2.0", 11020 + "icu_segmenter", 11068 11021 "indexmap", 11069 11022 "itertools 0.14.0", 11070 11023 "itoa", ··· 11109 11062 [[package]] 11110 11063 name = "stylo_atoms" 11111 11064 version = "0.16.0" 11112 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11065 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11113 11066 dependencies = [ 11114 11067 "string_cache", 11115 11068 "string_cache_codegen", ··· 11118 11071 [[package]] 11119 11072 name = "stylo_derive" 11120 11073 version = "0.16.0" 11121 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11074 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11122 11075 dependencies = [ 11123 11076 "darling", 11124 11077 "proc-macro2", ··· 11130 11083 [[package]] 11131 11084 name = "stylo_dom" 11132 11085 version = "0.16.0" 11133 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11086 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11134 11087 dependencies = [ 11135 11088 "bitflags 2.11.1", 11136 11089 "stylo_malloc_size_of", ··· 11139 11092 [[package]] 11140 11093 name = "stylo_malloc_size_of" 11141 11094 version = "0.16.0" 11142 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11095 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11143 11096 dependencies = [ 11144 11097 "app_units", 11145 11098 "cssparser", ··· 11156 11109 [[package]] 11157 11110 name = "stylo_static_prefs" 11158 11111 version = "0.16.0" 11159 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11112 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11160 11113 11161 11114 [[package]] 11162 11115 name = "stylo_traits" 11163 11116 version = "0.16.0" 11164 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11117 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11165 11118 dependencies = [ 11166 11119 "app_units", 11167 11120 "bitflags 2.11.1", ··· 11259 11212 checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 11260 11213 dependencies = [ 11261 11214 "proc-macro2", 11215 + "quote", 11262 11216 "unicode-ident", 11263 11217 ] 11264 11218 ··· 11666 11620 checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" 11667 11621 dependencies = [ 11668 11622 "displaydoc", 11669 - "serde_core", 11670 11623 "zerovec 0.11.6", 11671 11624 ] 11672 11625 ··· 11698 11651 [[package]] 11699 11652 name = "to_shmem" 11700 11653 version = "0.3.0" 11701 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11654 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11702 11655 dependencies = [ 11703 11656 "cssparser", 11704 11657 "servo_arc", ··· 11711 11664 [[package]] 11712 11665 name = "to_shmem_derive" 11713 11666 version = "0.1.0" 11714 - source = "git+https://github.com/servo/stylo?rev=a556f4cbd15fc289039261661b049a5dc845cd80#a556f4cbd15fc289039261661b049a5dc845cd80" 11667 + source = "git+https://github.com/servo/stylo?rev=bfa746c957ce12bf12936e118b78a806d9ae0d66#bfa746c957ce12bf12936e118b78a806d9ae0d66" 11715 11668 dependencies = [ 11716 11669 "darling", 11717 11670 "proc-macro2", ··· 12777 12730 "bytes", 12778 12731 "cookie 0.16.2", 12779 12732 "http 0.2.12", 12780 - "icu_segmenter 1.5.0", 12733 + "icu_segmenter", 12781 12734 "log", 12782 12735 "serde", 12783 12736 "serde_derive", ··· 14122 14075 "displaydoc", 14123 14076 "yoke 0.8.2", 14124 14077 "zerofrom", 14125 - "zerovec 0.11.6", 14126 14078 ] 14127 14079 14128 14080 [[package]] ··· 14142 14094 source = "registry+https://github.com/rust-lang/crates.io-index" 14143 14095 checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" 14144 14096 dependencies = [ 14145 - "serde", 14146 14097 "yoke 0.8.2", 14147 14098 "zerofrom", 14148 14099 "zerovec-derive 0.11.3",
+10 -9
Cargo.toml
··· 177 177 regex = "1.12" 178 178 resvg = "0.47.0" 179 179 rsa = { version = "0.9.10", features = ["sha1", "sha2"] } 180 + rusqlite = "0.38" 180 181 rustc-hash = "2.1.1" 181 182 rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] } 182 183 rustls-pki-types = "1.14" ··· 187 188 sea-query = { version = "1.0.0-rc.30", default-features = false, features = ["backend-sqlite", "derive"] } 188 189 sea-query-rusqlite = { version = "0.8.0-rc.15" } 189 190 sec1 = "0.7" 190 - selectors = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 191 + selectors = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 191 192 serde = "1.0.228" 192 193 serde_bytes = "0.11" 193 194 serde_core = "1.0.226" ··· 223 224 servo-wakelock = { version = "0.1.0", path = "source/components/wakelock" } 224 225 servo_allocator = { package = "servo-allocator", version = "0.1.0", path = "source/components/allocator" } 225 226 servo-tracing = { path = "source/components/servo_tracing" } 226 - servo_arc = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 227 + servo_arc = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 227 228 servo-url = { version = "0.1.0", path = "source/components/url" } 228 229 sha1 = "0.10" 229 230 sha2 = "0.10" ··· 235 236 storage_traits = { package = "servo-storage-traits", path = "source/components/shared/storage" } 236 237 string_cache = "0.9" 237 238 strum = { version = "0.27", features = ["derive"] } 238 - stylo = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 239 - stylo_atoms = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 240 - stylo_config = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 241 - stylo_dom = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 242 - stylo_malloc_size_of = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 243 - stylo_static_prefs = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 244 - stylo_traits = { git = "https://github.com/servo/stylo", rev = "a556f4cbd15fc289039261661b049a5dc845cd80" } 239 + stylo = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 240 + stylo_atoms = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 241 + stylo_config = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 242 + stylo_dom = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 243 + stylo_malloc_size_of = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 244 + stylo_static_prefs = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 245 + stylo_traits = { git = "https://github.com/servo/stylo", rev = "bfa746c957ce12bf12936e118b78a806d9ae0d66" } 245 246 surfman = { version = "0.12.0", features = ["chains"] } 246 247 syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] } 247 248 synstructure = "0.13"
+1
crates/beaver_shell/Cargo.toml
··· 41 41 log = { workspace = true } 42 42 mime_guess = { workspace = true } 43 43 parking_lot = { workspace = true } 44 + rusqlite = { workspace = true } 44 45 rustls = { workspace = true, features = ["aws-lc-rs"] } 45 46 serde = { workspace = true } 46 47 serde_json = { workspace = true }
+136 -5
crates/beaver_shell/src/content_blocker.rs
··· 7 7 use adblock::lists::{FilterSet, ParseOptions}; 8 8 use adblock::request::Request; 9 9 use content_security_policy::Destination; 10 - use log::{info, warn}; 10 + use log::{error, info, warn}; 11 + use rusqlite::Connection; 11 12 use servo::prefs::get_embedder_pref; 12 13 use servo::{ 13 14 FetchResponseMsg, PrefValue, Referrer, RequestBuilder, ResourceThreads, ServoUrl, ··· 15 16 }; 16 17 17 18 const CACHE_FILE: &str = "adblock-engine.bin"; 19 + const DB_FILE: &str = "content_blocker.db"; 18 20 const EASYLIST_URL: &str = "https://easylist.to/easylist/easylist.txt"; 19 21 const BUNDLED_EASYLIST: &str = include_str!("../resources/easylist.txt"); 20 22 23 + pub struct OriginState { 24 + pub enabled: bool, 25 + pub blocked_count: u32, 26 + pub allowed_count: u32, 27 + } 28 + 21 29 pub struct ContentBlocker { 22 30 engine: Engine, 23 31 pending_update: Arc<Mutex<Option<Vec<u8>>>>, 32 + db: Option<Connection>, 24 33 } 25 34 26 35 impl ContentBlocker { 27 36 pub fn new(config_dir: Option<PathBuf>) -> Self { 28 37 info!("[ContentBlocker] Initializing..."); 29 38 let engine = Self::load_or_build(&config_dir); 39 + let db = Self::open_db(&config_dir); 30 40 Self { 31 41 engine, 32 42 pending_update: Arc::new(Mutex::new(None)), 43 + db, 44 + } 45 + } 46 + 47 + fn open_db(config_dir: &Option<PathBuf>) -> Option<Connection> { 48 + let dir = config_dir.as_ref()?; 49 + let db_path = dir.join(DB_FILE); 50 + match Connection::open(&db_path) { 51 + Ok(conn) => { 52 + if let Err(e) = conn.execute_batch( 53 + "PRAGMA journal_mode=WAL; 54 + CREATE TABLE IF NOT EXISTS origin_state ( 55 + origin TEXT PRIMARY KEY, 56 + enabled INTEGER NOT NULL DEFAULT 1, 57 + blocked_count INTEGER NOT NULL DEFAULT 0, 58 + allowed_count INTEGER NOT NULL DEFAULT 0 59 + )", 60 + ) { 61 + error!("[ContentBlocker] Failed to create table: {e}"); 62 + return None; 63 + } 64 + Some(conn) 65 + }, 66 + Err(e) => { 67 + error!("[ContentBlocker] Failed to open database: {e}"); 68 + None 69 + }, 33 70 } 34 71 } 35 72 ··· 127 164 ) -> bool { 128 165 self.apply_pending_update(config_dir); 129 166 130 - if !self.is_enabled() { 167 + if !self.is_globally_enabled() { 168 + return false; 169 + } 170 + 171 + let source_origin = url::Url::parse(source_url) 172 + .map(|u| u.origin().ascii_serialization()) 173 + .unwrap_or_default(); 174 + 175 + if source_origin.is_empty() { 176 + return false; 177 + } 178 + 179 + if !self.is_origin_enabled(&source_origin) { 180 + self.increment_allowed(&source_origin); 131 181 return false; 132 182 } 133 183 ··· 137 187 }; 138 188 139 189 let result = self.engine.check_network_request(&request); 140 - result.matched && result.exception.is_none() 190 + let blocked = result.matched && result.exception.is_none(); 191 + 192 + if blocked { 193 + self.increment_blocked(&source_origin); 194 + } else { 195 + self.increment_allowed(&source_origin); 196 + } 197 + 198 + blocked 141 199 } 142 200 143 201 pub fn cosmetic_css_for_url(&self, url: &str) -> Option<String> { 144 - if !self.is_enabled() { 202 + if !self.is_globally_enabled() { 145 203 return None; 146 204 } 147 205 ··· 160 218 Some(format!("{css} {{ display: none !important; }}")) 161 219 } 162 220 163 - fn is_enabled(&self) -> bool { 221 + // --- Per-origin state --- 222 + 223 + pub fn is_origin_enabled(&self, origin: &str) -> bool { 224 + let Some(ref db) = self.db else { return true }; 225 + db.query_row( 226 + "SELECT enabled FROM origin_state WHERE origin = ?1", 227 + [origin], 228 + |row| row.get::<_, bool>(0), 229 + ) 230 + .unwrap_or(true) 231 + } 232 + 233 + pub fn set_origin_enabled(&self, origin: &str, enabled: bool) { 234 + let Some(ref db) = self.db else { return }; 235 + let _ = db.execute( 236 + "INSERT INTO origin_state (origin, enabled) VALUES (?1, ?2) 237 + ON CONFLICT(origin) DO UPDATE SET enabled = ?2", 238 + rusqlite::params![origin, enabled], 239 + ); 240 + } 241 + 242 + pub fn get_origin_state(&self, origin: &str) -> OriginState { 243 + let Some(ref db) = self.db else { 244 + return OriginState { 245 + enabled: true, 246 + blocked_count: 0, 247 + allowed_count: 0, 248 + }; 249 + }; 250 + db.query_row( 251 + "SELECT enabled, blocked_count, allowed_count FROM origin_state WHERE origin = ?1", 252 + [origin], 253 + |row| { 254 + Ok(OriginState { 255 + enabled: row.get(0)?, 256 + blocked_count: row.get(1)?, 257 + allowed_count: row.get(2)?, 258 + }) 259 + }, 260 + ) 261 + .unwrap_or(OriginState { 262 + enabled: true, 263 + blocked_count: 0, 264 + allowed_count: 0, 265 + }) 266 + } 267 + 268 + pub fn reset_counts_for_origin(&self, origin: &str) { 269 + let Some(ref db) = self.db else { return }; 270 + let _ = db.execute( 271 + "UPDATE origin_state SET blocked_count = 0, allowed_count = 0 WHERE origin = ?1", 272 + [origin], 273 + ); 274 + } 275 + 276 + fn increment_blocked(&self, origin: &str) { 277 + let Some(ref db) = self.db else { return }; 278 + let _ = db.execute( 279 + "INSERT INTO origin_state (origin, blocked_count) VALUES (?1, 1) 280 + ON CONFLICT(origin) DO UPDATE SET blocked_count = blocked_count + 1", 281 + [origin], 282 + ); 283 + } 284 + 285 + fn increment_allowed(&self, origin: &str) { 286 + let Some(ref db) = self.db else { return }; 287 + let _ = db.execute( 288 + "INSERT INTO origin_state (origin, allowed_count) VALUES (?1, 1) 289 + ON CONFLICT(origin) DO UPDATE SET allowed_count = allowed_count + 1", 290 + [origin], 291 + ); 292 + } 293 + 294 + fn is_globally_enabled(&self) -> bool { 164 295 matches!( 165 296 get_embedder_pref("beaver.content_blocking"), 166 297 Some(PrefValue::Bool(true))
+36
crates/beaver_shell/src/main.rs
··· 779 779 window.inject_input_event(event); 780 780 } 781 781 } 782 + 783 + fn content_blocker_get_origin_state( 784 + &self, 785 + origin: String, 786 + callback: servo::GenericCallback<Result<servo::ContentBlockerOriginState, String>>, 787 + ) { 788 + let state = self.content_blocker.borrow().get_origin_state(&origin); 789 + let _ = callback.send(Ok(servo::ContentBlockerOriginState { 790 + enabled: state.enabled, 791 + blocked_count: state.blocked_count, 792 + allowed_count: state.allowed_count, 793 + })); 794 + } 795 + 796 + fn content_blocker_set_origin_enabled( 797 + &self, 798 + origin: String, 799 + enabled: bool, 800 + callback: servo::GenericCallback<Result<(), String>>, 801 + ) { 802 + self.content_blocker 803 + .borrow() 804 + .set_origin_enabled(&origin, enabled); 805 + let _ = callback.send(Ok(())); 806 + } 807 + 808 + fn content_blocker_reset_counts( 809 + &self, 810 + origin: String, 811 + callback: servo::GenericCallback<Result<(), String>>, 812 + ) { 813 + self.content_blocker 814 + .borrow() 815 + .reset_counts_for_origin(&origin); 816 + let _ = callback.send(Ok(())); 817 + } 782 818 } 783 819 784 820 /// A minimal ServoDelegate used to break the reference cycle between
+42 -19
patches/components/constellation/constellation.rs.patch
··· 412 412 }, 413 413 #[cfg(feature = "webgpu")] 414 414 ScriptToConstellationMessage::RequestAdapter(response_sender, options, ids) => self 415 - @@ -2142,7 +2300,871 @@ 415 + @@ -2142,9 +2300,894 @@ 416 416 } 417 417 }, 418 418 }, ··· 613 613 + ScriptToConstellationMessage::PairingRequestGuestPairing(id, pin, callback) => { 614 614 + self.pairing.request_guest_pairing(&id, &pin, callback); 615 615 + }, 616 + + ScriptToConstellationMessage::ContentBlockerGetOriginState(origin, callback) => { 617 + + self.constellation_to_embedder_proxy.send( 618 + + ConstellationToEmbedderMsg::ContentBlockerGetOriginState(origin, callback), 619 + + ); 620 + + }, 621 + + ScriptToConstellationMessage::ContentBlockerSetOriginEnabled( 622 + + origin, 623 + + enabled, 624 + + callback, 625 + + ) => { 626 + + self.constellation_to_embedder_proxy.send( 627 + + ConstellationToEmbedderMsg::ContentBlockerSetOriginEnabled( 628 + + origin, enabled, callback, 629 + + ), 630 + + ); 631 + + }, 632 + + ScriptToConstellationMessage::ContentBlockerResetCounts(origin, callback) => { 633 + + self.constellation_to_embedder_proxy.send( 634 + + ConstellationToEmbedderMsg::ContentBlockerResetCounts(origin, callback), 635 + + ); 636 + + }, 616 637 + ScriptToConstellationMessage::CreatePeerStream( 617 638 + peer_id, 618 639 + local_port_id, ··· 961 982 + let _ = callback.send(None); 962 983 + } 963 984 + }, 964 - + } 965 - + } 966 - + 985 + } 986 + } 987 + 967 988 + fn handle_pairing_event(&mut self, event: PairingEvent) { 968 989 + if let PairingEvent::MessageReceived { ref from, ref data } = event { 969 990 + debug!("P2P message received from {from}, {} bytes", data.len()); ··· 1250 1271 + } 1251 1272 + } 1252 1273 + return; 1253 - } 1274 + + } 1254 1275 + 1255 1276 + // Handle peer disconnect: clean up remote channel state. 1256 1277 + if let PairingEvent::PeerExpired { ref id } = event { ··· 1281 1302 + let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone())); 1282 1303 + } 1283 1304 + } 1284 - } 1285 - 1305 + + } 1306 + + 1286 1307 /// Check the origin of a message against that of the pipeline it came from. 1287 - @@ -2461,6 +3483,55 @@ 1308 + /// Note: this is still limited as a security check, 1309 + /// see <https://github.com/servo/servo/issues/11722> 1310 + @@ -2461,6 +3504,55 @@ 1288 1311 TransferState::TransferInProgress(queue) => queue.push_back(task), 1289 1312 TransferState::CompletionFailed(queue) => queue.push_back(task), 1290 1313 TransferState::CompletionRequested(_, queue) => queue.push_back(task), ··· 1340 1363 } 1341 1364 } 1342 1365 1343 - @@ -3360,6 +4431,40 @@ 1366 + @@ -3360,6 +4452,40 @@ 1344 1367 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable> 1345 1368 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { 1346 1369 debug!("{webview_id}: Closing"); ··· 1381 1404 let browsing_context_id = BrowsingContextId::from(webview_id); 1382 1405 // Step 5. Remove traversable from the user agent's top-level traversable set. 1383 1406 let browsing_context = 1384 - @@ -3636,8 +4741,27 @@ 1407 + @@ -3636,8 +4762,27 @@ 1385 1408 opener_webview_id, 1386 1409 opener_pipeline_id, 1387 1410 response_sender, ··· 1409 1432 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 1410 1433 warn!("Failed to create channel"); 1411 1434 let _ = response_sender.send(None); 1412 - @@ -3736,6 +4860,395 @@ 1435 + @@ -3736,6 +4881,395 @@ 1413 1436 }); 1414 1437 } 1415 1438 ··· 1805 1828 #[servo_tracing::instrument(skip_all)] 1806 1829 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { 1807 1830 let Some(pipeline) = self.pipelines.get(&pipeline_id) else { 1808 - @@ -4285,7 +5798,7 @@ 1831 + @@ -4285,7 +5819,7 @@ 1809 1832 }, 1810 1833 }; 1811 1834 ··· 1814 1837 match self.browsing_contexts.get_mut(&browsing_context_id) { 1815 1838 Some(browsing_context) => { 1816 1839 let old_pipeline_id = browsing_context.pipeline_id; 1817 - @@ -4294,6 +5807,7 @@ 1840 + @@ -4294,6 +5828,7 @@ 1818 1841 old_pipeline_id, 1819 1842 browsing_context.parent_pipeline_id, 1820 1843 browsing_context.webview_id, ··· 1822 1845 ) 1823 1846 }, 1824 1847 None => { 1825 - @@ -4303,6 +5817,15 @@ 1848 + @@ -4303,6 +5838,15 @@ 1826 1849 1827 1850 self.unload_document(old_pipeline_id); 1828 1851 ··· 1838 1861 if let Some(new_pipeline) = self.pipelines.get(&new_pipeline_id) { 1839 1862 if let Some(ref chan) = self.devtools_sender { 1840 1863 let state = NavigationState::Start(new_pipeline.url.clone()); 1841 - @@ -4861,7 +6384,7 @@ 1864 + @@ -4861,7 +6405,7 @@ 1842 1865 } 1843 1866 1844 1867 #[servo_tracing::instrument(skip_all)] ··· 1847 1870 // Send a flat projection of the history to embedder. 1848 1871 // The final vector is a concatenation of the URLs of the past 1849 1872 // entries, the current entry and the future entries. 1850 - @@ -4965,9 +6488,22 @@ 1873 + @@ -4965,9 +6509,22 @@ 1851 1874 self.constellation_to_embedder_proxy 1852 1875 .send(ConstellationToEmbedderMsg::HistoryChanged( 1853 1876 webview_id, ··· 1871 1894 } 1872 1895 1873 1896 #[servo_tracing::instrument(skip_all)] 1874 - @@ -4986,7 +6522,7 @@ 1897 + @@ -4986,7 +6543,7 @@ 1875 1898 } 1876 1899 } 1877 1900 ··· 1880 1903 match self.browsing_contexts.get_mut(&change.browsing_context_id) { 1881 1904 Some(browsing_context) => { 1882 1905 debug!("Adding pipeline to existing browsing context."); 1883 - @@ -4993,11 +6529,15 @@ 1906 + @@ -4993,11 +6550,15 @@ 1884 1907 let old_pipeline_id = browsing_context.pipeline_id; 1885 1908 browsing_context.pipelines.insert(change.new_pipeline_id); 1886 1909 browsing_context.update_current_entry(change.new_pipeline_id); ··· 1898 1921 }, 1899 1922 }; 1900 1923 1901 - @@ -5005,6 +6545,18 @@ 1924 + @@ -5005,6 +6566,18 @@ 1902 1925 self.unload_document(old_pipeline_id); 1903 1926 } 1904 1927
+27
patches/components/constellation/embedder.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -6,8 +6,9 @@ 4 + InputEventOutcome, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId, 5 + MediaSessionEvent, NewWebViewDetails, TraversalId, 6 + }; 7 + -use servo_base::generic_channel::GenericSender; 8 + +use servo_base::generic_channel::{GenericCallback, GenericSender}; 9 + use servo_base::id::{PipelineId, WebViewId}; 10 + +use servo_constellation_traits::ContentBlockerOriginState; 11 + use servo_url::ServoUrl; 12 + 13 + /// Messages sent from the `Constellation` to the embedder. 14 + @@ -47,4 +48,13 @@ 15 + AllowNavigationRequest(WebViewId, PipelineId, ServoUrl), 16 + /// The history state has changed. 17 + HistoryChanged(WebViewId, Vec<ServoUrl>, usize), 18 + + /// Content blocker: get origin state. 19 + + ContentBlockerGetOriginState( 20 + + String, 21 + + GenericCallback<Result<ContentBlockerOriginState, String>>, 22 + + ), 23 + + /// Content blocker: set origin enabled. 24 + + ContentBlockerSetOriginEnabled(String, bool, GenericCallback<Result<(), String>>), 25 + + /// Content blocker: reset counts. 26 + + ContentBlockerResetCounts(String, GenericCallback<Result<(), String>>), 27 + }
+6 -1
patches/components/constellation/tracing.rs.patch
··· 36 36 Self::ActivateDocument => target!("ActivateDocument"), 37 37 Self::SetDocumentState(..) => target!("SetDocumentState"), 38 38 Self::SetFinalUrl(..) => target!("SetFinalUrl"), 39 - @@ -193,6 +201,64 @@ 39 + @@ -193,6 +201,69 @@ 40 40 Self::TriggerGarbageCollection => target!("TriggerGarbageCollection"), 41 41 Self::AcquireWakeLock(..) => target!("AcquireWakeLock"), 42 42 Self::ReleaseWakeLock(..) => target!("ReleaseWakeLock"), ··· 98 98 + Self::TaskProviderSelected(..) => target!("TaskProviderSelected"), 99 99 + Self::AcceptTask(..) => target!("AcceptTask"), 100 100 + Self::SetTaskDefault(..) => target!("SetTaskDefault"), 101 + + Self::ContentBlockerGetOriginState(..) => target!("ContentBlockerGetOriginState"), 102 + + Self::ContentBlockerSetOriginEnabled(..) => { 103 + + target!("ContentBlockerSetOriginEnabled") 104 + + }, 105 + + Self::ContentBlockerResetCounts(..) => target!("ContentBlockerResetCounts"), 101 106 } 102 107 } 103 108 }
+141
patches/components/script/dom/contentblocker.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,138 @@ 4 + +/* SPDX Id: AGPL-3.0-or-later */ 5 + + 6 + +use std::rc::Rc; 7 + + 8 + +use dom_struct::dom_struct; 9 + +use js::context::JSContext; 10 + +use script_bindings::error::Error; 11 + +use servo_constellation_traits::{ContentBlockerOriginState, ScriptToConstellationMessage}; 12 + + 13 + +use crate::dom::bindings::codegen::Bindings::ContentBlockerBinding::{ 14 + + ContentBlockerMethods, ContentBlockerOriginState as ContentBlockerOriginStateDict, 15 + +}; 16 + +use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; 17 + +use crate::dom::bindings::root::DomRoot; 18 + +use crate::dom::bindings::str::USVString; 19 + +use crate::dom::globalscope::GlobalScope; 20 + +use crate::dom::promise::Promise; 21 + +use crate::realms::InRealm; 22 + +use crate::routed_promise::{RoutedPromiseListener, callback_promise}; 23 + +use crate::script_runtime::CanGc; 24 + + 25 + +#[dom_struct] 26 + +pub(crate) struct ContentBlocker { 27 + + reflector_: crate::dom::bindings::reflector::Reflector, 28 + +} 29 + + 30 + +impl ContentBlocker { 31 + + fn new_inherited() -> ContentBlocker { 32 + + ContentBlocker { 33 + + reflector_: crate::dom::bindings::reflector::Reflector::new(), 34 + + } 35 + + } 36 + + 37 + + pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<ContentBlocker> { 38 + + reflect_dom_object(Box::new(ContentBlocker::new_inherited()), global, can_gc) 39 + + } 40 + +} 41 + + 42 + +impl ContentBlockerMethods<crate::DomTypeHolder> for ContentBlocker { 43 + + fn GetOriginState(&self, origin: USVString, comp: InRealm, can_gc: CanGc) -> Rc<Promise> { 44 + + let global = &self.global(); 45 + + let promise = Promise::new_in_current_realm(comp, can_gc); 46 + + let task_source = global.task_manager().dom_manipulation_task_source(); 47 + + let callback = callback_promise(&promise, self, task_source); 48 + + 49 + + let chan = global.script_to_constellation_chan(); 50 + + if chan 51 + + .send(ScriptToConstellationMessage::ContentBlockerGetOriginState( 52 + + origin.0, callback, 53 + + )) 54 + + .is_err() 55 + + { 56 + + promise.reject_error(Error::Operation(None), can_gc); 57 + + } 58 + + promise 59 + + } 60 + + 61 + + fn SetOriginEnabled( 62 + + &self, 63 + + origin: USVString, 64 + + enabled: bool, 65 + + comp: InRealm, 66 + + can_gc: CanGc, 67 + + ) -> Rc<Promise> { 68 + + let global = &self.global(); 69 + + let promise = Promise::new_in_current_realm(comp, can_gc); 70 + + let task_source = global.task_manager().dom_manipulation_task_source(); 71 + + let callback = callback_promise(&promise, self, task_source); 72 + + 73 + + let chan = global.script_to_constellation_chan(); 74 + + if chan 75 + + .send( 76 + + ScriptToConstellationMessage::ContentBlockerSetOriginEnabled( 77 + + origin.0, enabled, callback, 78 + + ), 79 + + ) 80 + + .is_err() 81 + + { 82 + + promise.reject_error(Error::Operation(None), can_gc); 83 + + } 84 + + promise 85 + + } 86 + + 87 + + fn ResetCounts(&self, origin: USVString, comp: InRealm, can_gc: CanGc) -> Rc<Promise> { 88 + + let global = &self.global(); 89 + + let promise = Promise::new_in_current_realm(comp, can_gc); 90 + + let task_source = global.task_manager().dom_manipulation_task_source(); 91 + + let callback = callback_promise(&promise, self, task_source); 92 + + 93 + + let chan = global.script_to_constellation_chan(); 94 + + if chan 95 + + .send(ScriptToConstellationMessage::ContentBlockerResetCounts( 96 + + origin.0, callback, 97 + + )) 98 + + .is_err() 99 + + { 100 + + promise.reject_error(Error::Operation(None), can_gc); 101 + + } 102 + + promise 103 + + } 104 + +} 105 + + 106 + +impl RoutedPromiseListener<Result<ContentBlockerOriginState, String>> for ContentBlocker { 107 + + fn handle_response( 108 + + &self, 109 + + cx: &mut JSContext, 110 + + response: Result<ContentBlockerOriginState, String>, 111 + + promise: &Rc<Promise>, 112 + + ) { 113 + + match response { 114 + + Ok(state) => { 115 + + let dict = ContentBlockerOriginStateDict { 116 + + enabled: state.enabled, 117 + + blockedCount: state.blocked_count, 118 + + allowedCount: state.allowed_count, 119 + + }; 120 + + promise.resolve_native(&dict, CanGc::from_cx(cx)); 121 + + }, 122 + + Err(msg) => { 123 + + promise.reject_error(Error::Operation(Some(msg)), CanGc::from_cx(cx)); 124 + + }, 125 + + } 126 + + } 127 + +} 128 + + 129 + +impl RoutedPromiseListener<Result<(), String>> for ContentBlocker { 130 + + fn handle_response( 131 + + &self, 132 + + cx: &mut JSContext, 133 + + response: Result<(), String>, 134 + + promise: &Rc<Promise>, 135 + + ) { 136 + + match response { 137 + + Ok(()) => promise.resolve_native(&(), CanGc::from_cx(cx)), 138 + + Err(msg) => promise.reject_error(Error::Operation(Some(msg)), CanGc::from_cx(cx)), 139 + + } 140 + + } 141 + +}
+2 -2
patches/components/script/dom/document/document_event_handler.rs.patch
··· 756 756 + NamedKey::ArrowLeft | 757 757 + NamedKey::ArrowRight, 758 758 + ) => { 759 - + if super::spatial_navigation::handle_arrow_key(&document, node, event, can_gc) { 759 + + if super::spatial_navigation::handle_arrow_key(cx, &document, event) { 760 760 + return; 761 761 + } 762 762 + }, 763 763 + Key::Named(NamedKey::Enter) => { 764 - + if super::spatial_navigation::handle_enter_key(&document, node, event, can_gc) { 764 + + if super::spatial_navigation::handle_enter_key(cx, &document) { 765 765 + return; 766 766 + } 767 767 + },
+16 -18
patches/components/script/dom/document/spatial_navigation.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,327 @@ 3 + @@ -0,0 +1,325 @@ 4 4 +/* SPDX Id: AGPL-3.0-or-later */ 5 5 + 6 6 +//! Spatial navigation: navigate to the nearest focusable element using arrow keys. ··· 56 56 +/// Handle an arrow key press for spatial navigation. 57 57 +/// Returns true if the event was handled (spatial nav is enabled and a candidate was found). 58 58 +pub(crate) fn handle_arrow_key( 59 + + cx: &mut js::context::JSContext, 59 60 + document: &Document, 60 - + _node: &Node, 61 61 + event: &KeyboardEvent, 62 - + can_gc: CanGc, 63 62 +) -> bool { 64 63 + if !document.spatial_navigation_enabled() { 65 64 + return false; ··· 74 73 + return false; 75 74 + }; 76 75 + 77 - + spatial_focus_navigation(document, direction, can_gc) 76 + + spatial_focus_navigation(cx, document, direction) 78 77 +} 79 78 + 80 79 +/// Handle Enter key for spatial navigation activation. 81 80 +/// Returns true if the event was handled. 82 - +pub(crate) fn handle_enter_key( 83 - + document: &Document, 84 - + _node: &Node, 85 - + _event: &KeyboardEvent, 86 - + can_gc: CanGc, 87 - +) -> bool { 81 + +pub(crate) fn handle_enter_key(cx: &mut js::context::JSContext, document: &Document) -> bool { 88 82 + if !document.spatial_navigation_enabled() { 89 83 + return false; 90 84 + } ··· 107 101 + // Simulate click on the focused element 108 102 + if let Some(html_element) = focused.downcast::<HTMLElement>() { 109 103 + debug!("[SpatialNav] Enter: clicking <{}>", focused.local_name()); 110 - + html_element.Click(can_gc); 104 + + html_element.Click(CanGc::from_cx(cx)); 111 105 + return true; 112 106 + } 113 107 + ··· 138 132 + 139 133 +/// Move focus to the nearest focusable element in the given direction. 140 134 +/// Returns true if focus was moved. 141 - +fn spatial_focus_navigation(document: &Document, direction: Direction, can_gc: CanGc) -> bool { 135 + +fn spatial_focus_navigation( 136 + + cx: &mut js::context::JSContext, 137 + + document: &Document, 138 + + direction: Direction, 139 + +) -> bool { 142 140 + // Get the currently focused element and its rect 143 141 + let focused: Option<DomRoot<Element>> = document 144 142 + .focus_handler() ··· 158 156 + // If no current focus, pick the first focusable element 159 157 + if current_rect.is_none() { 160 158 + debug!("[SpatialNav] No current focus, picking first element"); 161 - + return focus_first_element(document, can_gc); 159 + + return focus_first_element(cx, document); 162 160 + } 163 161 + 164 162 + let current_rect = current_rect.unwrap(); ··· 229 227 + winner.local_name(), 230 228 + score 231 229 + ); 232 - + focus_element(winner, can_gc); 230 + + focus_element(cx, winner); 233 231 + true 234 232 + } else { 235 233 + debug!( ··· 241 239 +} 242 240 + 243 241 +/// When no element is focused, pick a reasonable starting element. 244 - +fn focus_first_element(document: &Document, can_gc: CanGc) -> bool { 242 + +fn focus_first_element(cx: &mut js::context::JSContext, document: &Document) -> bool { 245 243 + let Some(root) = document.GetDocumentElement() else { 246 244 + debug!("[SpatialNav] focus_first: no document element"); 247 245 + return false; ··· 262 260 + element.local_name(), 263 261 + count 264 262 + ); 265 - + focus_element(element, can_gc); 263 + + focus_element(cx, element); 266 264 + return true; 267 265 + } 268 266 + count += 1; ··· 312 310 +} 313 311 + 314 312 +/// Focus an element and scroll it into view. 315 - +fn focus_element(element: &Element, can_gc: CanGc) { 313 + +fn focus_element(cx: &mut js::context::JSContext, element: &Element) { 316 314 + element 317 315 + .upcast::<Node>() 318 - + .run_the_focusing_steps(None, can_gc); 316 + + .run_the_focusing_steps(None, CanGc::from_cx(cx)); 319 317 + let scroll_axis = ScrollAxisState { 320 318 + position: ScrollLogicalPosition::Center, 321 319 + requirement: ScrollRequirement::IfNotVisible,
+9 -1
patches/components/script/dom/embedder.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,537 @@ 3 + @@ -0,0 +1,545 @@ 4 4 +/* SPDX Id: AGPL-3.0-or-later */ 5 5 + 6 6 +//! The `Embedder` interface provides communication between web content and the embedder. ··· 31 31 +use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; 32 32 +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; 33 33 +use crate::dom::bindings::str::DOMString; 34 + +use crate::dom::contentblocker::ContentBlocker; 34 35 +use crate::dom::customevent::CustomEvent; 35 36 +use crate::dom::event::Event; 36 37 +use crate::dom::eventtarget::EventTarget; ··· 43 44 +pub(crate) struct Embedder { 44 45 + eventtarget: EventTarget, 45 46 + pairing: MutNullableDom<Pairing>, 47 + + content_blocker: MutNullableDom<ContentBlocker>, 46 48 +} 47 49 + 48 50 +impl Embedder { ··· 50 52 + Embedder { 51 53 + eventtarget: EventTarget::new_inherited(), 52 54 + pairing: Default::default(), 55 + + content_blocker: Default::default(), 53 56 + } 54 57 + } 55 58 + ··· 516 519 + fn Pairing(&self, can_gc: CanGc) -> DomRoot<Pairing> { 517 520 + self.pairing 518 521 + .or_init(|| Pairing::new(&self.global(), can_gc)) 522 + + } 523 + + 524 + + fn ContentBlocker(&self, can_gc: CanGc) -> DomRoot<ContentBlocker> { 525 + + self.content_blocker 526 + + .or_init(|| ContentBlocker::new(&self.global(), can_gc)) 519 527 + } 520 528 + 521 529 + // Event handler for servo error events
+11 -3
patches/components/script/dom/mod.rs.patch
··· 8 8 pub(crate) mod attr; 9 9 pub(crate) mod audio; 10 10 pub(crate) use self::audio::*; 11 - @@ -271,6 +272,7 @@ 11 + @@ -236,6 +237,7 @@ 12 + pub(crate) use self::clipboard::*; 13 + pub(crate) mod comment; 14 + pub(crate) mod console; 15 + +pub(crate) mod contentblocker; 16 + pub(crate) mod cookiestore; 17 + mod create; 18 + pub(crate) mod credentialmanagement; 19 + @@ -271,6 +273,7 @@ 12 20 pub(crate) mod elementinternals; 13 21 pub(crate) mod encoding; 14 22 pub(crate) use self::encoding::*; ··· 16 24 pub(crate) mod event; 17 25 pub(crate) use self::event::*; 18 26 pub(crate) mod eventsource; 19 - @@ -296,6 +298,7 @@ 27 + @@ -296,6 +299,7 @@ 20 28 pub(crate) use self::indexeddb::*; 21 29 pub(crate) mod intersectionobserver; 22 30 pub(crate) mod intersectionobserverentry; ··· 24 32 pub(crate) mod location; 25 33 pub(crate) mod media; 26 34 pub(crate) use self::media::*; 27 - @@ -317,6 +320,10 @@ 35 + @@ -317,6 +321,10 @@ 28 36 pub(crate) mod origin; 29 37 pub(crate) mod paintsize; 30 38 pub(crate) mod paintworkletglobalscope;
+17 -5
patches/components/script_bindings/codegen/Bindings.conf.patch
··· 12 12 'Attr': { 13 13 'implicitCxSetters': True, 14 14 }, 15 - @@ -325,6 +330,11 @@ 15 + @@ -118,6 +123,11 @@ 16 + 'cx': ['Constructor'], 17 + }, 18 + 19 + +'ContentBlocker': { 20 + + 'inRealms': ['GetOriginState', 'SetOriginEnabled', 'ResetCounts'], 21 + + 'canGc': ['GetOriginState', 'SetOriginEnabled', 'ResetCounts'], 22 + +}, 23 + + 24 + 'CookieStore': { 25 + 'cx': ['Set', 'Set_', 'Get', 'Get_', 'GetAll', 'GetAll_', 'Delete', 'Delete_'], 26 + }, 27 + @@ -325,6 +335,11 @@ 16 28 'cx': ['CheckValidity', 'ReportValidity'], 17 29 }, 18 30 19 31 +'Embedder': { 20 32 + 'additionalTraits': ['crate::interfaces::EmbedderHelpers'], 21 - + 'canGc': ['Pairing'], 33 + + 'canGc': ['Pairing', 'ContentBlocker'], 22 34 +}, 23 35 + 24 36 'EventSource': { 25 37 'weakReferenceable': True, 26 38 }, 27 - @@ -751,9 +761,9 @@ 39 + @@ -751,9 +766,9 @@ 28 40 }, 29 41 30 42 'Navigator': { ··· 37 49 }, 38 50 39 51 'WakeLock': { 40 - @@ -793,6 +803,11 @@ 52 + @@ -793,6 +808,11 @@ 41 53 'cx': ['RegisterPaint'], 42 54 }, 43 55 ··· 49 61 'PerformanceObserver': { 50 62 'canGc': ['SupportedEntryTypes'], 51 63 }, 52 - @@ -966,7 +981,8 @@ 64 + @@ -966,7 +986,8 @@ 53 65 }, 54 66 55 67 'Window': {
+28
patches/components/script_bindings/webidls/ContentBlocker.webidl.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,25 @@ 4 + +/* SPDX Id: AGPL-3.0-or-later */ 5 + + 6 + +[Exposed=Window, 7 + +Func="Embedder::is_allowed_to_embed"] 8 + +interface ContentBlocker { 9 + + // Get blocking state for an origin 10 + + Promise<ContentBlockerOriginState> getOriginState(USVString origin); 11 + + 12 + + // Enable/disable blocking for a specific origin 13 + + Promise<undefined> setOriginEnabled(USVString origin, boolean enabled); 14 + + 15 + + // Reset counts for an origin 16 + + Promise<undefined> resetCounts(USVString origin); 17 + +}; 18 + + 19 + +dictionary ContentBlockerOriginState { 20 + + boolean enabled = true; 21 + + unsigned long blockedCount = 0; 22 + + unsigned long allowedCount = 0; 23 + +}; 24 + + 25 + +partial interface Embedder { 26 + + [Func="Embedder::is_allowed_to_embed"] 27 + + readonly attribute ContentBlocker contentBlocker; 28 + +};
+5 -2
patches/components/servo/lib.rs.patch
··· 14 14 pub use paint::WebRenderDebugOption; 15 15 pub use paint_api::rendering_context::{ 16 16 OffscreenRenderingContext, RenderingContext, SoftwareRenderingContext, WindowRenderingContext, 17 - @@ -56,10 +58,13 @@ 17 + @@ -55,11 +57,15 @@ 18 + // This should be replaced with an API on ServoBuilder. 18 19 // See <https://github.com/servo/servo/issues/40950>. 19 20 pub use resources; 20 - pub use servo_base::generic_channel::GenericSender; 21 + -pub use servo_base::generic_channel::GenericSender; 21 22 -pub use servo_base::id::WebViewId; 23 + +pub use servo_base::generic_channel::{GenericCallback, GenericSender}; 22 24 +pub use servo_base::id::{BrowsingContextId, PipelineId, WebViewId}; 23 25 +pub use servo_config::embedder_prefs::PrefsPersistError; 24 26 pub use servo_config::opts::{DiagnosticsLogging, Opts, OutputOptions}; ··· 27 29 +pub use servo_config::{ 28 30 + EmbedderPreferences, define_embedder_prefs, embedder_prefs, opts, pref, prefs, 29 31 +}; 32 + +pub use servo_constellation_traits::ContentBlockerOriginState; 30 33 pub use servo_geometry::{ 31 34 DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel, 32 35 };
+32 -6
patches/components/servo/servo.rs.patch
··· 153 153 } 154 154 } 155 155 156 - @@ -951,6 +1027,7 @@ 156 + @@ -807,6 +883,25 @@ 157 + .notify_media_session_event(webview, media_session_event); 158 + } 159 + }, 160 + + ConstellationToEmbedderMsg::ContentBlockerGetOriginState(origin, callback) => { 161 + + self.delegate 162 + + .borrow() 163 + + .content_blocker_get_origin_state(origin, callback); 164 + + }, 165 + + ConstellationToEmbedderMsg::ContentBlockerSetOriginEnabled( 166 + + origin, 167 + + enabled, 168 + + callback, 169 + + ) => { 170 + + self.delegate 171 + + .borrow() 172 + + .content_blocker_set_origin_enabled(origin, enabled, callback); 173 + + }, 174 + + ConstellationToEmbedderMsg::ContentBlockerResetCounts(origin, callback) => { 175 + + self.delegate 176 + + .borrow() 177 + + .content_blocker_reset_counts(origin, callback); 178 + + }, 179 + } 180 + } 181 + } 182 + @@ -951,6 +1046,7 @@ 157 183 async_runtime, 158 184 public_storage_threads.clone(), 159 185 private_storage_threads.clone(), ··· 161 187 ); 162 188 163 189 net::connector::prewarm_tls(); 164 - @@ -962,6 +1039,7 @@ 190 + @@ -962,6 +1058,7 @@ 165 191 Servo(Rc::new(ServoInner { 166 192 delegate: RefCell::new(Rc::new(DefaultServoDelegate)), 167 193 paint, ··· 169 195 network_manager: Rc::new(RefCell::new(NetworkManager::new( 170 196 public_resource_threads.clone(), 171 197 private_resource_threads.clone(), 172 - @@ -996,6 +1074,10 @@ 198 + @@ -996,6 +1093,10 @@ 173 199 *self.0.delegate.borrow_mut() = delegate; 174 200 } 175 201 ··· 180 206 /// **EXPERIMENTAL:** Intialize GL accelerated media playback. This currently only works on a limited number 181 207 /// of platforms. This should be run *before* creating [`Servo`] and its first [`WebView`]. 182 208 pub fn initialize_gl_accelerated_media(display: NativeDisplay, api: GlApi, context: GlContext) { 183 - @@ -1050,6 +1132,14 @@ 209 + @@ -1050,6 +1151,14 @@ 184 210 &self.0.site_data_manager 185 211 } 186 212 ··· 195 221 pub(crate) fn paint<'a>(&'a self) -> Ref<'a, Paint> { 196 222 self.0.paint.borrow() 197 223 } 198 - @@ -1152,6 +1242,7 @@ 224 + @@ -1152,6 +1261,7 @@ 199 225 async_runtime: Box<dyn net_traits::AsyncRuntime>, 200 226 public_storage_threads: StorageThreads, 201 227 private_storage_threads: StorageThreads, ··· 203 229 ) { 204 230 // Global configuration options, parsed from the command line. 205 231 let opts = opts::get(); 206 - @@ -1195,6 +1286,7 @@ 232 + @@ -1195,6 +1305,7 @@ 207 233 async_runtime, 208 234 privileged_urls, 209 235 wake_lock_provider: Box::new(DefaultWakeLockDelegate),
+33 -3
patches/components/servo/servo_delegate.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -1,8 +1,10 @@ 3 + @@ -1,8 +1,11 @@ 4 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 6 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 7 7 -use embedder_traits::{ConsoleLogLevel, Notification}; 8 + -use servo_base::generic_channel; 8 9 +use embedder_traits::{ConsoleLogLevel, InputEvent, Notification}; 9 - use servo_base::generic_channel; 10 + +use servo_base::generic_channel::{self, GenericCallback}; 10 11 +use servo_base::id::WebViewId; 12 + +use servo_constellation_traits::ContentBlockerOriginState; 11 13 +use url::Url; 12 14 13 15 use crate::webview_delegate::{AllowOrDenyRequest, WebResourceLoad}; 14 16 15 - @@ -44,6 +46,27 @@ 17 + @@ -44,6 +47,55 @@ 16 18 /// 17 19 /// <https://developer.mozilla.org/en-US/docs/Web/API/Console_API> 18 20 fn show_console_message(&self, _level: ConsoleLogLevel, _message: String) {} ··· 37 39 + 38 40 + /// Used to inject a keyboard event as if from a real keyboard. 39 41 + fn inject_input_event(&self, _event: InputEvent) {} 42 + + 43 + + /// Content blocker: get origin state (enabled, blocked/allowed counts). 44 + + fn content_blocker_get_origin_state( 45 + + &self, 46 + + _origin: String, 47 + + callback: GenericCallback<Result<ContentBlockerOriginState, String>>, 48 + + ) { 49 + + let _ = callback.send(Err("Not implemented".to_owned())); 50 + + } 51 + + 52 + + /// Content blocker: enable/disable blocking for a specific origin. 53 + + fn content_blocker_set_origin_enabled( 54 + + &self, 55 + + _origin: String, 56 + + _enabled: bool, 57 + + callback: GenericCallback<Result<(), String>>, 58 + + ) { 59 + + let _ = callback.send(Err("Not implemented".to_owned())); 60 + + } 61 + + 62 + + /// Content blocker: reset counts for an origin. 63 + + fn content_blocker_reset_counts( 64 + + &self, 65 + + _origin: String, 66 + + callback: GenericCallback<Result<(), String>>, 67 + + ) { 68 + + let _ = callback.send(Err("Not implemented".to_owned())); 69 + + } 40 70 } 41 71 42 72 pub(crate) struct DefaultServoDelegate;
+10 -1
patches/components/shared/constellation/from_script_message.rs.patch
··· 210 210 /// Mark a new document as active 211 211 ActivateDocument, 212 212 /// Set the document state for a pipeline (used by screenshot / reftests) 213 - @@ -754,6 +886,122 @@ 213 + @@ -754,6 +886,131 @@ 214 214 /// aggregate lock count and notify the provider only when the count transitions from N to 0. 215 215 /// <https://w3c.github.io/screen-wake-lock/#dfn-release-wake-lock> 216 216 ReleaseWakeLock(WakeLockType), ··· 282 282 + PairingDisableGuestMode(GenericCallback<Result<(), String>>), 283 283 + /// Request guest pairing with a PIN. 284 284 + PairingRequestGuestPairing(String, String, GenericCallback<Result<bool, String>>), 285 + + /// Content blocker: get origin state (enabled, blocked_count, allowed_count). 286 + + ContentBlockerGetOriginState( 287 + + String, 288 + + GenericCallback<Result<super::ContentBlockerOriginState, String>>, 289 + + ), 290 + + /// Content blocker: enable/disable blocking for a specific origin. 291 + + ContentBlockerSetOriginEnabled(String, bool, GenericCallback<Result<(), String>>), 292 + + /// Content blocker: reset counts for an origin. 293 + + ContentBlockerResetCounts(String, GenericCallback<Result<(), String>>), 285 294 + /// Create a peer stream: create a virtual remote port entangled with a local port, 286 295 + /// and send the offer to a remote peer. 287 296 + /// Args: peer_id, local_port_id, remote_port_id, target_url, callback.
+10 -2
patches/components/shared/constellation/lib.rs.patch
··· 19 19 }; 20 20 pub use from_script_message::*; 21 21 use malloc_size_of_derive::MallocSizeOf; 22 - @@ -30,15 +32,144 @@ 22 + @@ -30,15 +32,152 @@ 23 23 use rustc_hash::FxHashMap; 24 24 use serde::{Deserialize, Serialize}; 25 25 use servo_base::cross_process_instant::CrossProcessInstant; ··· 95 95 + pub name: String, 96 96 +} 97 97 + 98 + +/// Content blocker state for a specific origin. 99 + +#[derive(Clone, Debug, Deserialize, Serialize)] 100 + +pub struct ContentBlockerOriginState { 101 + + pub enabled: bool, 102 + + pub blocked_count: u32, 103 + + pub allowed_count: u32, 104 + +} 105 + + 98 106 +/// Information about a remote P2P peer. 99 107 +#[derive(Clone, Debug, Deserialize, Serialize)] 100 108 +pub struct PeerInfo { ··· 166 174 /// Messages to the Constellation from the embedding layer, whether from `ServoRenderer` or 167 175 /// from `libservo` itself. 168 176 #[derive(IntoStaticStr)] 169 - @@ -118,6 +249,9 @@ 177 + @@ -118,6 +257,9 @@ 170 178 UpdatePinchZoomInfos(PipelineId, PinchZoomInfos), 171 179 /// Activate or deactivate accessibility features for the given `WebView`. 172 180 SetAccessibilityActive(WebViewId, bool),
+215
ui/system/content_blocker_panel.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + LitElement, 5 + html, 6 + css, 7 + } from "beaver://shared/third_party/lit/lit-all.min.js"; 8 + 9 + class ContentBlockerPanel extends LitElement { 10 + static properties = { 11 + open: { type: Boolean, reflect: true }, 12 + origin: { type: String }, 13 + enabled: { type: Boolean }, 14 + blockedCount: { type: Number }, 15 + allowedCount: { type: Number }, 16 + x: { type: Number }, 17 + y: { type: Number }, 18 + }; 19 + 20 + static styles = css` 21 + :host { 22 + display: none; 23 + } 24 + 25 + :host([open]) { 26 + display: block; 27 + position: absolute; 28 + inset: 0; 29 + z-index: 10000; 30 + } 31 + 32 + .backdrop { 33 + position: fixed; 34 + inset: 0; 35 + z-index: -1; 36 + } 37 + 38 + .panel { 39 + position: absolute; 40 + background: var(--bg-menu); 41 + border: 1px solid var(--color-border); 42 + border-radius: var(--radius-md); 43 + padding: var(--spacing-md); 44 + min-width: 220px; 45 + box-shadow: 0 4px 16px var(--color-shadow-menu); 46 + font-family: var(--font-family-base); 47 + display: flex; 48 + flex-direction: column; 49 + gap: var(--spacing-sm); 50 + animation: panel-in 0.18s cubic-bezier(0.16, 1, 0.3, 1); 51 + } 52 + 53 + @keyframes panel-in { 54 + from { 55 + opacity: 0; 56 + transform: translateY(-4px); 57 + } 58 + } 59 + 60 + .origin { 61 + font-size: var(--font-size-sm); 62 + color: var(--color-text-secondary); 63 + white-space: nowrap; 64 + overflow: hidden; 65 + text-overflow: ellipsis; 66 + } 67 + 68 + .toggle-row { 69 + display: flex; 70 + align-items: center; 71 + justify-content: space-between; 72 + gap: var(--spacing-md); 73 + } 74 + 75 + .toggle-label { 76 + font-size: var(--font-size-sm); 77 + font-weight: var(--font-weight-bold); 78 + } 79 + 80 + .toggle-switch { 81 + position: relative; 82 + width: 2.4em; 83 + height: 1.4em; 84 + flex-shrink: 0; 85 + } 86 + 87 + .toggle-switch input { 88 + opacity: 0; 89 + width: 0; 90 + height: 0; 91 + } 92 + 93 + .toggle-slider { 94 + position: absolute; 95 + inset: 0; 96 + background: var(--color-border); 97 + border-radius: 0.7em; 98 + cursor: pointer; 99 + transition: background var(--transition-fast); 100 + } 101 + 102 + .toggle-slider::before { 103 + content: ""; 104 + position: absolute; 105 + width: 1em; 106 + height: 1em; 107 + left: 0.2em; 108 + bottom: 0.2em; 109 + background: var(--color-text-on-header); 110 + border-radius: 50%; 111 + transition: transform var(--transition-fast); 112 + } 113 + 114 + .toggle-switch input:checked + .toggle-slider { 115 + background: var(--color-primary); 116 + } 117 + 118 + .toggle-switch input:checked + .toggle-slider::before { 119 + transform: translateX(1em); 120 + } 121 + 122 + .toggle-switch input:focus-visible + .toggle-slider { 123 + outline: 2px solid var(--color-focus-ring); 124 + outline-offset: 2px; 125 + } 126 + 127 + .stats { 128 + font-size: var(--font-size-xs); 129 + color: var(--color-text-tertiary); 130 + } 131 + 132 + .stats .count { 133 + color: var(--color-text-secondary); 134 + font-weight: var(--font-weight-bold); 135 + } 136 + `; 137 + 138 + constructor() { 139 + super(); 140 + this.open = false; 141 + this.origin = ""; 142 + this.enabled = true; 143 + this.blockedCount = 0; 144 + this.allowedCount = 0; 145 + this.x = 0; 146 + this.y = 0; 147 + } 148 + 149 + async handleToggle(e) { 150 + const newEnabled = e.target.checked; 151 + this.enabled = newEnabled; 152 + try { 153 + await navigator.embedder.contentBlocker.setOriginEnabled( 154 + this.origin, 155 + newEnabled, 156 + ); 157 + this.dispatchEvent( 158 + new CustomEvent("blocker-toggled", { 159 + bubbles: true, 160 + composed: true, 161 + detail: { enabled: newEnabled }, 162 + }), 163 + ); 164 + } catch (err) { 165 + console.error("[ContentBlockerPanel] setOriginEnabled failed:", err); 166 + this.enabled = !newEnabled; 167 + } 168 + } 169 + 170 + dismiss() { 171 + this.open = false; 172 + this.dispatchEvent( 173 + new CustomEvent("panel-dismiss", { bubbles: true, composed: true }), 174 + ); 175 + } 176 + 177 + render() { 178 + if (!this.open) return html``; 179 + 180 + const total = this.blockedCount + this.allowedCount; 181 + const percentage = 182 + total > 0 ? Math.round((this.blockedCount / total) * 100) : 0; 183 + 184 + let hostname = this.origin; 185 + try { 186 + hostname = new URL(this.origin).hostname; 187 + } catch {} 188 + 189 + const panelStyle = `right: calc(100% - ${this.x}px); top: ${this.y}px;`; 190 + 191 + return html` 192 + <div class="backdrop" @click=${this.dismiss}></div> 193 + <div class="panel" style="${panelStyle}"> 194 + <div class="origin">${hostname}</div> 195 + <div class="toggle-row"> 196 + <span class="toggle-label">Content blocking</span> 197 + <label class="toggle-switch"> 198 + <input 199 + type="checkbox" 200 + .checked=${this.enabled} 201 + @change=${this.handleToggle} 202 + /> 203 + <span class="toggle-slider"></span> 204 + </label> 205 + </div> 206 + <div class="stats"> 207 + <span class="count">${this.blockedCount}</span> blocked / 208 + ${total} total (${percentage}%) 209 + </div> 210 + </div> 211 + `; 212 + } 213 + } 214 + 215 + customElements.define("content-blocker-panel", ContentBlockerPanel);
+125
ui/system/mobile_action_bar.css
··· 152 152 position: relative; 153 153 } 154 154 155 + .blocker-button { 156 + position: relative; 157 + } 158 + 159 + .blocker-button lucide-icon { 160 + color: var(--color-primary); 161 + } 162 + 163 + .blocker-button.disabled lucide-icon { 164 + color: var(--color-text-secondary); 165 + } 166 + 167 + .blocker-count { 168 + position: absolute; 169 + top: -4px; 170 + right: -4px; 171 + background: var(--color-primary); 172 + color: var(--color-text-on-header); 173 + font-size: 10px; 174 + min-width: 16px; 175 + height: 16px; 176 + border-radius: 8px; 177 + display: flex; 178 + align-items: center; 179 + justify-content: center; 180 + padding: 0 4px; 181 + font-weight: var(--font-weight-bold); 182 + } 183 + 155 184 /* Search Results — visually separated from the bar */ 156 185 .results-area { 157 186 max-height: 40vh; ··· 232 261 .result-text { 233 262 color: var(--color-text); 234 263 } 264 + 265 + /* ===== Content Blocker Inline Panel ===== */ 266 + 267 + .blocker-inline-panel { 268 + padding: var(--spacing-sm) var(--spacing-md); 269 + border-bottom: 1px solid var(--color-border); 270 + display: flex; 271 + flex-direction: column; 272 + gap: var(--spacing-sm); 273 + animation: blocker-panel-in 0.3s cubic-bezier(0.16, 1, 0.3, 1); 274 + } 275 + 276 + @keyframes blocker-panel-in { 277 + from { 278 + opacity: 0; 279 + max-height: 0; 280 + padding-top: 0; 281 + padding-bottom: 0; 282 + } 283 + to { 284 + opacity: 1; 285 + max-height: 6em; 286 + } 287 + } 288 + 289 + .blocker-inline-origin { 290 + font-size: var(--font-size-sm); 291 + color: var(--color-text-secondary); 292 + } 293 + 294 + .blocker-inline-row { 295 + display: flex; 296 + align-items: center; 297 + justify-content: space-between; 298 + } 299 + 300 + .blocker-inline-label { 301 + font-size: var(--font-size-sm); 302 + font-weight: var(--font-weight-bold); 303 + } 304 + 305 + .blocker-inline-toggle { 306 + position: relative; 307 + width: 2.4em; 308 + height: 1.4em; 309 + } 310 + 311 + .blocker-inline-toggle input { 312 + opacity: 0; 313 + width: 0; 314 + height: 0; 315 + } 316 + 317 + .blocker-inline-slider { 318 + position: absolute; 319 + inset: 0; 320 + background: var(--color-border); 321 + border-radius: 0.7em; 322 + cursor: pointer; 323 + transition: background var(--transition-fast); 324 + } 325 + 326 + .blocker-inline-slider::before { 327 + content: ""; 328 + position: absolute; 329 + width: 1em; 330 + height: 1em; 331 + left: 0.2em; 332 + bottom: 0.2em; 333 + background: var(--color-text-on-header); 334 + border-radius: 50%; 335 + transition: transform var(--transition-fast); 336 + } 337 + 338 + .blocker-inline-toggle input:checked + .blocker-inline-slider { 339 + background: var(--color-primary); 340 + } 341 + 342 + .blocker-inline-toggle input:checked + .blocker-inline-slider::before { 343 + transform: translateX(1em); 344 + } 345 + 346 + .blocker-inline-toggle input:focus-visible + .blocker-inline-slider { 347 + outline: 2px solid var(--color-focus-ring); 348 + outline-offset: 2px; 349 + } 350 + 351 + .blocker-inline-stats { 352 + font-size: var(--font-size-xs); 353 + color: var(--color-text-tertiary); 354 + } 355 + 356 + .blocker-inline-count { 357 + color: var(--color-text-secondary); 358 + font-weight: var(--font-weight-bold); 359 + }
+103 -1
ui/system/mobile_action_bar.js
··· 15 15 canGoBack: { type: Boolean }, 16 16 canGoForward: { type: Boolean }, 17 17 viewCount: { type: Number }, 18 + blockerCount: { type: Number }, 19 + blockerEnabled: { type: Boolean }, 20 + blockerPanelOpen: { type: Boolean }, 21 + blockerOrigin: { type: String }, 22 + blockerAllowed: { type: Number }, 18 23 results: { type: Array }, 19 24 groups: { type: Array }, 20 25 }; ··· 92 97 93 98 updateState() { 94 99 if (this.layoutManager) { 95 - this.url = this.layoutManager.getCurrentUrl(); 100 + const newUrl = this.layoutManager.getCurrentUrl(); 101 + if (newUrl !== this._lastUrl) { 102 + this._lastUrl = newUrl; 103 + this._needsBaselineReset = true; 104 + } 105 + this.url = newUrl; 96 106 const navState = this.layoutManager.getNavigationState(); 97 107 this.canGoBack = navState.canGoBack; 98 108 this.canGoForward = navState.canGoForward; 99 109 this.viewCount = this.layoutManager.getTabCount(); 110 + this.updateBlockerState(); 111 + } 112 + } 113 + 114 + async updateBlockerState() { 115 + if (!this.url || !this.url.startsWith("http")) { 116 + this.blockerCount = 0; 117 + this.blockerEnabled = true; 118 + this.blockerOrigin = ""; 119 + this.blockerAllowed = 0; 120 + return; 121 + } 122 + try { 123 + const origin = new URL(this.url).origin; 124 + this.blockerOrigin = origin; 125 + const state = 126 + await navigator.embedder.contentBlocker.getOriginState(origin); 127 + this.blockerCount = state.blockedCount; 128 + this.blockerEnabled = state.enabled; 129 + this.blockerAllowed = state.allowedCount; 130 + if (this._needsBaselineReset) { 131 + this._needsBaselineReset = false; 132 + this._blockerBaseline = state.blockedCount; 133 + } 134 + } catch { 135 + this.blockerCount = 0; 136 + this.blockerEnabled = true; 137 + this.blockerOrigin = ""; 138 + this.blockerAllowed = 0; 100 139 } 101 140 } 102 141 ··· 186 225 } 187 226 } 188 227 228 + handleBlocker() { 229 + if (this.blockerOrigin) { 230 + this.blockerPanelOpen = !this.blockerPanelOpen; 231 + } 232 + } 233 + 234 + async handleBlockerToggleInline(e) { 235 + const newEnabled = e.target.checked; 236 + this.blockerEnabled = newEnabled; 237 + try { 238 + await navigator.embedder.contentBlocker.setOriginEnabled( 239 + this.blockerOrigin, 240 + newEnabled, 241 + ); 242 + } catch { 243 + this.blockerEnabled = !newEnabled; 244 + } 245 + } 246 + 189 247 handleViews() { 190 248 if (this.layoutManager) { 191 249 this.layoutManager.toggleOverview(); ··· 268 326 /> 269 327 </div> 270 328 329 + ${this.blockerPanelOpen 330 + ? html` 331 + <div class="blocker-inline-panel"> 332 + <div class="blocker-inline-origin">${this.blockerHostname()}</div> 333 + <div class="blocker-inline-row"> 334 + <span class="blocker-inline-label">Content blocking</span> 335 + <label class="blocker-inline-toggle"> 336 + <input 337 + type="checkbox" 338 + .checked=${this.blockerEnabled} 339 + @change=${this.handleBlockerToggleInline} 340 + /> 341 + <span class="blocker-inline-slider"></span> 342 + </label> 343 + </div> 344 + <div class="blocker-inline-stats"> 345 + <span class="blocker-inline-count">${this.blockerCount}</span> blocked / 346 + ${this.blockerCount + this.blockerAllowed} total 347 + (${this.blockerPercentage()}%) 348 + </div> 349 + </div> 350 + ` 351 + : ""} 352 + 271 353 <div class="quick-actions"> 272 354 <button class="action-button" @click=${this.handleHome}> 273 355 <lucide-icon name="house"></lucide-icon> ··· 291 373 292 374 <button class="action-button" @click=${this.handleReload}> 293 375 <lucide-icon name="rotate-ccw"></lucide-icon> 376 + </button> 377 + 378 + <button class="action-button blocker-button ${this.blockerEnabled ? "" : "disabled"}" @click=${this.handleBlocker}> 379 + <lucide-icon name="shield"></lucide-icon> 380 + ${this.blockerCount - (this._blockerBaseline || 0) > 0 381 + ? html`<span class="blocker-count">${this.blockerCount - (this._blockerBaseline || 0)}</span>` 382 + : ""} 294 383 </button> 295 384 296 385 <button class="action-button views-button" @click=${this.handleViews}> ··· 302 391 </div> 303 392 </div> 304 393 `; 394 + } 395 + 396 + blockerHostname() { 397 + try { 398 + return new URL(this.blockerOrigin).hostname; 399 + } catch { 400 + return this.blockerOrigin; 401 + } 402 + } 403 + 404 + blockerPercentage() { 405 + const total = this.blockerCount + this.blockerAllowed; 406 + return total > 0 ? Math.round((this.blockerCount / total) * 100) : 0; 305 407 } 306 408 } 307 409
+41
ui/system/web_view.css
··· 94 94 align-items: center; 95 95 } 96 96 97 + :host .blocker-icon { 98 + position: relative; 99 + display: flex; 100 + align-items: center; 101 + cursor: pointer; 102 + padding: 0 0.4em; 103 + border-radius: var(--radius-sm); 104 + transition: background var(--transition-fast); 105 + } 106 + 107 + :host .blocker-icon:hover { 108 + background: var(--bg-hover); 109 + } 110 + 111 + :host .blocker-icon lucide-icon { 112 + font-size: 1.1em; 113 + color: var(--color-primary); 114 + transition: color var(--transition-fast); 115 + } 116 + 117 + :host .blocker-icon.disabled lucide-icon { 118 + color: var(--color-text-secondary); 119 + } 120 + 121 + :host .blocker-badge { 122 + position: absolute; 123 + top: -4px; 124 + right: -2px; 125 + min-width: 14px; 126 + height: 14px; 127 + border-radius: 7px; 128 + background: var(--color-primary); 129 + color: var(--color-text-on-header); 130 + font-size: 9px; 131 + font-weight: var(--font-weight-bold); 132 + display: flex; 133 + align-items: center; 134 + justify-content: center; 135 + padding: 0 3px; 136 + } 137 + 97 138 /* Dialog overlay styles - positioned within iframe-container */ 98 139 :host .dialog-overlay { 99 140 position: absolute;
+93
ui/system/web_view.js
··· 8 8 import "./url_bar_overlay.js"; 9 9 import "./context_menu.js"; 10 10 import "./select_control.js"; 11 + import "./content_blocker_panel.js"; 11 12 import "./color_picker.js"; 12 13 13 14 export class WebView extends LitElement { ··· 46 47 47 48 // Inline task provider state 48 49 this.inlineProvider = null; 50 + 51 + // Content blocker state 52 + this.blockerOrigin = ""; 53 + this.blockerEnabled = true; 54 + this.blockerCount = 0; 55 + this.blockerAllowed = 0; 56 + this.blockerBaseline = 0; 57 + this.blockerPanelOpen = false; 58 + this._needsBaselineReset = true; 59 + } 60 + 61 + get blockerBadgeCount() { 62 + return Math.max(0, this.blockerCount - this.blockerBaseline); 49 63 } 50 64 51 65 handleMenuAction(e) { ··· 140 154 colorPicker: { state: true }, 141 155 loadStatus: { state: true }, 142 156 inlineProvider: { state: true }, 157 + blockerOrigin: { state: true }, 158 + blockerEnabled: { state: true }, 159 + blockerCount: { state: true }, 160 + blockerAllowed: { state: true }, 161 + blockerPanelOpen: { state: true }, 162 + blockerPanelX: { state: true }, 163 + blockerPanelY: { state: true }, 143 164 }; 144 165 145 166 ensureIframe() { ··· 192 213 onloadstatuschange(event) { 193 214 console.log("[WebView] Load status changed:", event.detail); 194 215 this.loadStatus = event.detail; 216 + 217 + if (event.detail === "started") { 218 + this._needsBaselineReset = true; 219 + } 195 220 196 221 // Apply pending spatial navigation after load completes 197 222 if (event.detail === "complete") { ··· 966 991 // Capture screenshot for overview mode after a short delay 967 992 // to allow the page to render 968 993 this.captureScreenshot(); 994 + 995 + this.updateBlockerStatus(); 996 + } 997 + 998 + async updateBlockerStatus() { 999 + if (!this.currentUrl || !this.currentUrl.startsWith("http")) { 1000 + this.blockerOrigin = ""; 1001 + return; 1002 + } 1003 + try { 1004 + const origin = new URL(this.currentUrl).origin; 1005 + this.blockerOrigin = origin; 1006 + const state = 1007 + await navigator.embedder.contentBlocker.getOriginState(origin); 1008 + this.blockerEnabled = state.enabled; 1009 + this.blockerCount = state.blockedCount; 1010 + this.blockerAllowed = state.allowedCount; 1011 + if (this._needsBaselineReset) { 1012 + this._needsBaselineReset = false; 1013 + this.blockerBaseline = state.blockedCount; 1014 + } 1015 + } catch { 1016 + this.blockerOrigin = ""; 1017 + } 1018 + } 1019 + 1020 + handleBlockerClick(e) { 1021 + e.stopPropagation(); 1022 + if (!this.blockerPanelOpen) { 1023 + const buttonRect = e.currentTarget.getBoundingClientRect(); 1024 + const hostRect = this.getBoundingClientRect(); 1025 + this.blockerPanelX = buttonRect.right - hostRect.left; 1026 + this.blockerPanelY = buttonRect.bottom - hostRect.top; 1027 + } 1028 + this.blockerPanelOpen = !this.blockerPanelOpen; 1029 + } 1030 + 1031 + handleBlockerPanelDismiss() { 1032 + this.blockerPanelOpen = false; 1033 + this.updateBlockerStatus(); 1034 + } 1035 + 1036 + handleBlockerToggled(e) { 1037 + this.blockerEnabled = e.detail.enabled; 969 1038 } 970 1039 971 1040 updated(_changedProperties) { ··· 1203 1272 @click="${this.doReload}" 1204 1273 ></lucide-icon> 1205 1274 <span class="title" @click=${this.openUrlBar}>${this.title}</span> 1275 + ${this.blockerOrigin 1276 + ? html` 1277 + <div 1278 + class="blocker-icon ${this.blockerEnabled ? "" : "disabled"}" 1279 + @click=${this.handleBlockerClick} 1280 + > 1281 + <lucide-icon name="shield"></lucide-icon> 1282 + ${this.blockerBadgeCount > 0 1283 + ? html`<span class="blocker-badge">${this.blockerBadgeCount}</span>` 1284 + : ""} 1285 + </div> 1286 + ` 1287 + : ""} 1206 1288 <div class="menu-container"> 1207 1289 <lucide-icon 1208 1290 @click=${this.toggleMenu} ··· 1235 1317 @menu-cancel=${this.handleContextMenuCancel} 1236 1318 @menu-closed=${this.handleContextMenuClosed} 1237 1319 ></context-menu> 1320 + <content-blocker-panel 1321 + ?open=${this.blockerPanelOpen} 1322 + .origin=${this.blockerOrigin} 1323 + .enabled=${this.blockerEnabled} 1324 + .blockedCount=${this.blockerCount} 1325 + .allowedCount=${this.blockerAllowed} 1326 + .x=${this.blockerPanelX || 0} 1327 + .y=${this.blockerPanelY || 0} 1328 + @panel-dismiss=${this.handleBlockerPanelDismiss} 1329 + @blocker-toggled=${this.handleBlockerToggled} 1330 + ></content-blocker-panel> 1238 1331 ${this.renderUrlBarOverlay()} 1239 1332 <div class="iframe-container"> 1240 1333 <iframe