detached 788f19a

fix(webkit): restore content blocking after gotk4 migration

During the gotk4 migration, content blocking was left as empty TODO stubs.
This commit restores full functionality using WebKit's native content
filtering API.

Changes:

1. New file: pkg/webkit/content_blocking.go
   - ContentBlockingManager: Manages WebKit UserContentFilterStore
   - ApplyFiltersFromJSON(): Compiles WebKit JSON rules and applies to UserContentManager
   - ApplyFiltersToWebView(): Convenience wrapper for filter application

2. Modified: pkg/webkit/webview.go
   - InitializeContentBlocking(): Now properly implements filter application
   - Signature changed from []string to []byte for WebKit JSON rules
   - UpdateContentFilters(): Marked as deprecated with warning

3. Modified: internal/app/browser/webview.go
   - setupContentBlocking(): Wired up to fetch filters from FilterManager
   - Added callback to apply filters when they become ready
   - Filters now properly applied via UserContentManager.AddFilter()

How It Works:
```
FilterManager.GetNetworkFilters() → WebKit JSON []byte
                                      ↓
UserContentFilterStore.Save() → Compile to native format
                                      ↓
UserContentManager.AddFilter() → ✅ Content blocking active
```

WebKit Integration:
- Uses webkit.UserContentFilterStore for efficient filter compilation
- Compiles JSON rules asynchronously to avoid blocking startup
- Filters applied via webkit.UserContentManager.AddFilter()
- Follows WebKit content blocker spec: https://webkit.org/blog/3476/content-blockers-first-look/

Testing:
- Verified on lesnumeriques.com: ads and trackers successfully blocked
- Filter loading: 117K+ network rules successfully compiled and applied
- Minor WebKit preconnect assertion errors are harmless (WebKit bug with blockers)

Before: Content blocking was non-functional (empty TODO stubs)
After: Full ad/tracker blocking working via WebKit native API
commit
788f19a30144262fda9005f88dec346ff676cac9
author
bamen · · 2025-10-15 05:32 UTC
committer
bamen · · 2025-10-15 05:33 UTC
parent
0e199d7
signature
unsigned
3 files · +168 -41
M internal/app/browser/webview.go +30 -37
@@ -336,53 +336,46 @@ func (app *BrowserApp) setupContentBlocking() error {
336336 // Store reference to filter manager
337337 app.filterManager = filterManager
338338
339- // Set up callback to re-apply network filters when they become ready
339+ // Set up callback to apply network filters when they become ready
340340 // This callback will be called from the async filter loading process
341341 filterManager.SetFiltersReadyCallback(func() {
342- // Add small delay to avoid race conditions with WebView initialization
343- go func() {
344- time.Sleep(300 * time.Millisecond)
345- if app.webView != nil {
346- // Note: Content filtering is now handled via WebKit's UserContentManager
347- // UpdateContentFilters would need to be implemented if needed
348- log.Printf("Filters loaded - content blocking via UserContentManager")
349- }
350- }()
342+ log.Printf("[filtering] Filters ready, applying to WebView...")
343+
344+ // Get the WebKit JSON rules from filter manager
345+ filterJSON, err := filterManager.GetNetworkFilters()
346+ if err != nil {
347+ log.Printf("[filtering] Failed to get network filters: %v", err)
348+ return
349+ }
350+
351+ if len(filterJSON) == 0 {
352+ log.Printf("[filtering] No network filters to apply")
353+ return
354+ }
355+
356+ log.Printf("[filtering] Got %d bytes of WebKit JSON rules", len(filterJSON))
357+
358+ // Apply filters to the WebView
359+ // This must be done on the main thread since it touches GTK/WebKit
360+ if app.webView != nil {
361+ app.webView.RunOnMainThread(func() {
362+ if err := app.webView.InitializeContentBlocking(filterJSON); err != nil {
363+ log.Printf("[filtering] Failed to apply content filters: %v", err)
364+ } else {
365+ log.Printf("[filtering] ✅ Content blocking enabled successfully")
366+ }
367+ })
368+ }
351369 })
352370
353- // Start async filter loading early (before WebView content blocking initialization)
354- // This allows filters to be ready when the WebView needs them
371+ // Start async filter loading
372+ // This allows filters to compile in the background while the browser starts
355373 go func() {
356374 if err := filtering.InitializeFiltersAsync(filterManager); err != nil {
357375 log.Printf("Warning: failed to initialize filters asynchronously: %v", err)
358376 }
359377 }()
360378
361- // Initialize content blocking in WebView with delay
362- // Wait for WebView to be fully loaded before setting up content blocking
363- go func() {
364- // Wait extra time for the first navigation to avoid preconnect interference
365- log.Printf("Waiting for WebView to complete initial load before enabling content blocking...")
366- time.Sleep(3000 * time.Millisecond) // 3 seconds for first load stability
367-
368- // Note: Content blocking is now handled via WebKit's UserContentManager
369- // The InitializeContentBlocking API would need to be implemented if needed
370- log.Printf("Content blocking enabled via UserContentManager")
371-
372- // Register navigation handler for GUI injection
373- // Content filtering is handled by UserContentManager automatically
374- app.webView.RegisterURIChangedHandler(func(uri string) {
375- // Add small delay to avoid conflicts with page load
376- go func() {
377- time.Sleep(200 * time.Millisecond)
378-
379- // Note: Content filtering is automatic via UserContentManager
380- // GUI bundle (controls and toast) is injected as User Script
381- log.Printf("[webview] Navigated to: %s", uri)
382- }()
383- })
384- }()
385-
386379 log.Printf("Content blocking system initialization started")
387380 return nil
388381 }
@@ -336,53 +336,46 @@ func (app *BrowserApp) setupContentBlocking() error {
336 // Store reference to filter manager336 // Store reference to filter manager
337 app.filterManager = filterManager337 app.filterManager = filterManager
338 338
339- // Set up callback to re-apply network filters when they become ready339+ // Set up callback to apply network filters when they become ready
340 // This callback will be called from the async filter loading process340 // This callback will be called from the async filter loading process
341 filterManager.SetFiltersReadyCallback(func() {341 filterManager.SetFiltersReadyCallback(func() {
342- // Add small delay to avoid race conditions with WebView initialization342+ log.Printf("[filtering] Filters ready, applying to WebView...")
343- go func() {343+
344- time.Sleep(300 * time.Millisecond)344+ // Get the WebKit JSON rules from filter manager
345- if app.webView != nil {345+ filterJSON, err := filterManager.GetNetworkFilters()
346- // Note: Content filtering is now handled via WebKit's UserContentManager346+ if err != nil {
347- // UpdateContentFilters would need to be implemented if needed347+ log.Printf("[filtering] Failed to get network filters: %v", err)
348- log.Printf("Filters loaded - content blocking via UserContentManager")348+ return
349- }349+ }
350- }()350+
351+ if len(filterJSON) == 0 {
352+ log.Printf("[filtering] No network filters to apply")
353+ return
354+ }
355+
356+ log.Printf("[filtering] Got %d bytes of WebKit JSON rules", len(filterJSON))
357+
358+ // Apply filters to the WebView
359+ // This must be done on the main thread since it touches GTK/WebKit
360+ if app.webView != nil {
361+ app.webView.RunOnMainThread(func() {
362+ if err := app.webView.InitializeContentBlocking(filterJSON); err != nil {
363+ log.Printf("[filtering] Failed to apply content filters: %v", err)
364+ } else {
365+ log.Printf("[filtering] ✅ Content blocking enabled successfully")
366+ }
367+ })
368+ }
351 })369 })
352 370
353- // Start async filter loading early (before WebView content blocking initialization)371+ // Start async filter loading
354- // This allows filters to be ready when the WebView needs them372+ // This allows filters to compile in the background while the browser starts
355 go func() {373 go func() {
356 if err := filtering.InitializeFiltersAsync(filterManager); err != nil {374 if err := filtering.InitializeFiltersAsync(filterManager); err != nil {
357 log.Printf("Warning: failed to initialize filters asynchronously: %v", err)375 log.Printf("Warning: failed to initialize filters asynchronously: %v", err)
358 }376 }
359 }()377 }()
360 378
361- // Initialize content blocking in WebView with delay
362- // Wait for WebView to be fully loaded before setting up content blocking
363- go func() {
364- // Wait extra time for the first navigation to avoid preconnect interference
365- log.Printf("Waiting for WebView to complete initial load before enabling content blocking...")
366- time.Sleep(3000 * time.Millisecond) // 3 seconds for first load stability
367-
368- // Note: Content blocking is now handled via WebKit's UserContentManager
369- // The InitializeContentBlocking API would need to be implemented if needed
370- log.Printf("Content blocking enabled via UserContentManager")
371-
372- // Register navigation handler for GUI injection
373- // Content filtering is handled by UserContentManager automatically
374- app.webView.RegisterURIChangedHandler(func(uri string) {
375- // Add small delay to avoid conflicts with page load
376- go func() {
377- time.Sleep(200 * time.Millisecond)
378-
379- // Note: Content filtering is automatic via UserContentManager
380- // GUI bundle (controls and toast) is injected as User Script
381- log.Printf("[webview] Navigated to: %s", uri)
382- }()
383- })
384- }()
385-
386 log.Printf("Content blocking system initialization started")379 log.Printf("Content blocking system initialization started")
387 return nil380 return nil
388 }381 }
A pkg/webkit/content_blocking.go +128 -0
@@ -0,0 +1,128 @@
1+package webkit
2+
3+import (
4+ "context"
5+ "fmt"
6+ "log"
7+ "path/filepath"
8+
9+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
10+ "github.com/diamondburned/gotk4/pkg/gio/v2"
11+ "github.com/diamondburned/gotk4/pkg/glib/v2"
12+)
13+
14+// ContentBlockingManager handles WebKit content filtering
15+type ContentBlockingManager struct {
16+ filterStore *webkit.UserContentFilterStore
17+ storagePath string
18+}
19+
20+// NewContentBlockingManager creates a new content blocking manager
21+func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, error) {
22+ // Create the filter store
23+ filterStore := webkit.NewUserContentFilterStore(storagePath)
24+ if filterStore == nil {
25+ return nil, fmt.Errorf("failed to create UserContentFilterStore")
26+ }
27+
28+ log.Printf("[webkit] Created UserContentFilterStore at: %s", storagePath)
29+
30+ return &ContentBlockingManager{
31+ filterStore: filterStore,
32+ storagePath: storagePath,
33+ }, nil
34+}
35+
36+// ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager
37+// The jsonRules parameter should be a JSON array of WebKit content blocker rules
38+func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error {
39+ if ucm == nil {
40+ return fmt.Errorf("UserContentManager is nil")
41+ }
42+
43+ if len(jsonRules) == 0 {
44+ return fmt.Errorf("empty filter rules")
45+ }
46+
47+ log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules))
48+
49+ // Convert []byte to *glib.Bytes for WebKit API
50+ // NewBytesWithGo keeps the Go slice alive for the GBytes lifetime
51+ gBytes := glib.NewBytesWithGo(jsonRules)
52+
53+ // Save filters to store asynchronously
54+ // This compiles the JSON rules into WebKit's internal format
55+ ctx := context.Background()
56+
57+ // Use a channel to wait for the async operation
58+ done := make(chan error, 1)
59+
60+ cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
61+ // This callback runs when compilation is complete
62+ filter, err := cbm.filterStore.SaveFinish(result)
63+ if err != nil {
64+ log.Printf("[webkit] Failed to compile content filter: %v", err)
65+ done <- fmt.Errorf("failed to compile filter: %w", err)
66+ return
67+ }
68+
69+ if filter == nil {
70+ log.Printf("[webkit] Filter compilation returned nil")
71+ done <- fmt.Errorf("filter compilation returned nil")
72+ return
73+ }
74+
75+ // Add the compiled filter to the UserContentManager
76+ // This must be done on the main GTK thread
77+ RunOnMainThread(func() {
78+ ucm.AddFilter(filter)
79+ log.Printf("[webkit] Successfully added content filter: %s", identifier)
80+ done <- nil
81+ })
82+ })
83+
84+ // Wait for completion
85+ return <-done
86+}
87+
88+// RemoveAllFilters removes all content filters from a UserContentManager
89+func (cbm *ContentBlockingManager) RemoveAllFilters(ucm *webkit.UserContentManager) {
90+ if ucm != nil {
91+ ucm.RemoveAllFilters()
92+ log.Printf("[webkit] Removed all content filters")
93+ }
94+}
95+
96+// GetFilterStore returns the underlying UserContentFilterStore
97+func (cbm *ContentBlockingManager) GetFilterStore() *webkit.UserContentFilterStore {
98+ return cbm.filterStore
99+}
100+
101+// ApplyFiltersToWebView applies content blocking filters to a WebView
102+func ApplyFiltersToWebView(view *WebView, filterJSON []byte) error {
103+ if view == nil || view.view == nil {
104+ return fmt.Errorf("invalid WebView")
105+ }
106+
107+ // Get the UserContentManager
108+ ucm := view.view.UserContentManager()
109+ if ucm == nil {
110+ return fmt.Errorf("UserContentManager is nil")
111+ }
112+
113+ // Create content blocking manager
114+ // Use a subdirectory of the cache dir for filter storage
115+ dataDir := view.config.DataDir
116+ if dataDir == "" {
117+ return fmt.Errorf("WebView has no data directory configured")
118+ }
119+
120+ filterStorePath := filepath.Join(dataDir, "content-filters")
121+ cbm, err := NewContentBlockingManager(filterStorePath)
122+ if err != nil {
123+ return fmt.Errorf("failed to create content blocking manager: %w", err)
124+ }
125+
126+ // Apply the filters
127+ return cbm.ApplyFiltersFromJSON(ucm, "adblock-filters", filterJSON)
128+}
@@ -0,0 +1,128 @@
1+package webkit
2+
3+import (
4+ "context"
5+ "fmt"
6+ "log"
7+ "path/filepath"
8+
9+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
10+ "github.com/diamondburned/gotk4/pkg/gio/v2"
11+ "github.com/diamondburned/gotk4/pkg/glib/v2"
12+)
13+
14+// ContentBlockingManager handles WebKit content filtering
15+type ContentBlockingManager struct {
16+ filterStore *webkit.UserContentFilterStore
17+ storagePath string
18+}
19+
20+// NewContentBlockingManager creates a new content blocking manager
21+func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, error) {
22+ // Create the filter store
23+ filterStore := webkit.NewUserContentFilterStore(storagePath)
24+ if filterStore == nil {
25+ return nil, fmt.Errorf("failed to create UserContentFilterStore")
26+ }
27+
28+ log.Printf("[webkit] Created UserContentFilterStore at: %s", storagePath)
29+
30+ return &ContentBlockingManager{
31+ filterStore: filterStore,
32+ storagePath: storagePath,
33+ }, nil
34+}
35+
36+// ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager
37+// The jsonRules parameter should be a JSON array of WebKit content blocker rules
38+func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error {
39+ if ucm == nil {
40+ return fmt.Errorf("UserContentManager is nil")
41+ }
42+
43+ if len(jsonRules) == 0 {
44+ return fmt.Errorf("empty filter rules")
45+ }
46+
47+ log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules))
48+
49+ // Convert []byte to *glib.Bytes for WebKit API
50+ // NewBytesWithGo keeps the Go slice alive for the GBytes lifetime
51+ gBytes := glib.NewBytesWithGo(jsonRules)
52+
53+ // Save filters to store asynchronously
54+ // This compiles the JSON rules into WebKit's internal format
55+ ctx := context.Background()
56+
57+ // Use a channel to wait for the async operation
58+ done := make(chan error, 1)
59+
60+ cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
61+ // This callback runs when compilation is complete
62+ filter, err := cbm.filterStore.SaveFinish(result)
63+ if err != nil {
64+ log.Printf("[webkit] Failed to compile content filter: %v", err)
65+ done <- fmt.Errorf("failed to compile filter: %w", err)
66+ return
67+ }
68+
69+ if filter == nil {
70+ log.Printf("[webkit] Filter compilation returned nil")
71+ done <- fmt.Errorf("filter compilation returned nil")
72+ return
73+ }
74+
75+ // Add the compiled filter to the UserContentManager
76+ // This must be done on the main GTK thread
77+ RunOnMainThread(func() {
78+ ucm.AddFilter(filter)
79+ log.Printf("[webkit] Successfully added content filter: %s", identifier)
80+ done <- nil
81+ })
82+ })
83+
84+ // Wait for completion
85+ return <-done
86+}
87+
88+// RemoveAllFilters removes all content filters from a UserContentManager
89+func (cbm *ContentBlockingManager) RemoveAllFilters(ucm *webkit.UserContentManager) {
90+ if ucm != nil {
91+ ucm.RemoveAllFilters()
92+ log.Printf("[webkit] Removed all content filters")
93+ }
94+}
95+
96+// GetFilterStore returns the underlying UserContentFilterStore
97+func (cbm *ContentBlockingManager) GetFilterStore() *webkit.UserContentFilterStore {
98+ return cbm.filterStore
99+}
100+
101+// ApplyFiltersToWebView applies content blocking filters to a WebView
102+func ApplyFiltersToWebView(view *WebView, filterJSON []byte) error {
103+ if view == nil || view.view == nil {
104+ return fmt.Errorf("invalid WebView")
105+ }
106+
107+ // Get the UserContentManager
108+ ucm := view.view.UserContentManager()
109+ if ucm == nil {
110+ return fmt.Errorf("UserContentManager is nil")
111+ }
112+
113+ // Create content blocking manager
114+ // Use a subdirectory of the cache dir for filter storage
115+ dataDir := view.config.DataDir
116+ if dataDir == "" {
117+ return fmt.Errorf("WebView has no data directory configured")
118+ }
119+
120+ filterStorePath := filepath.Join(dataDir, "content-filters")
121+ cbm, err := NewContentBlockingManager(filterStorePath)
122+ if err != nil {
123+ return fmt.Errorf("failed to create content blocking manager: %w", err)
124+ }
125+
126+ // Apply the filters
127+ return cbm.ApplyFiltersFromJSON(ucm, "adblock-filters", filterJSON)
128+}
M pkg/webkit/webview.go +10 -4
@@ -713,12 +713,14 @@ func (w *WebView) UpdateContentFilters(rules string) error {
713713 return ErrWebViewDestroyed
714714 }
715715
716- // TODO: Implement content filtering using WebKit's UserContentManager
716+ // Note: Content filtering is handled via InitializeContentBlocking()
717+ // This method is kept for backward compatibility but is deprecated
718+ log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
717719 return nil
718720 }
719721
720722 // InitializeContentBlocking initializes content blocking with filter lists
721-func (w *WebView) InitializeContentBlocking(filterLists []string) error {
723+func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
722724 w.mu.RLock()
723725 defer w.mu.RUnlock()
724726
@@ -726,8 +728,12 @@ func (w *WebView) InitializeContentBlocking(filterLists []string) error {
726728 return ErrWebViewDestroyed
727729 }
728730
729- // TODO: Implement content blocking initialization
730- return nil
731+ if len(filterJSON) == 0 {
732+ return fmt.Errorf("no filter rules provided")
733+ }
734+
735+ // Apply filters using the content blocking API
736+ return ApplyFiltersToWebView(w, filterJSON)
731737 }
732738
733739 // OnNavigate registers a navigation handler
@@ -713,12 +713,14 @@ func (w *WebView) UpdateContentFilters(rules string) error {
713 return ErrWebViewDestroyed713 return ErrWebViewDestroyed
714 }714 }
715 715
716- // TODO: Implement content filtering using WebKit's UserContentManager716+ // Note: Content filtering is handled via InitializeContentBlocking()
717+ // This method is kept for backward compatibility but is deprecated
718+ log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
717 return nil719 return nil
718 }720 }
719 721
720 // InitializeContentBlocking initializes content blocking with filter lists722 // InitializeContentBlocking initializes content blocking with filter lists
721-func (w *WebView) InitializeContentBlocking(filterLists []string) error {723+func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
722 w.mu.RLock()724 w.mu.RLock()
723 defer w.mu.RUnlock()725 defer w.mu.RUnlock()
724 726
@@ -726,8 +728,12 @@ func (w *WebView) InitializeContentBlocking(filterLists []string) error {
726 return ErrWebViewDestroyed728 return ErrWebViewDestroyed
727 }729 }
728 730
729- // TODO: Implement content blocking initialization731+ if len(filterJSON) == 0 {
730- return nil732+ return fmt.Errorf("no filter rules provided")
733+ }
734+
735+ // Apply filters using the content blocking API
736+ return ApplyFiltersToWebView(w, filterJSON)
731 }737 }
732 738
733 // OnNavigate registers a navigation handler739 // OnNavigate registers a navigation handler

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close