1package webkit
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "sync"
8
9 "github.com/bnema/dumber/internal/logging"
10 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
11 "github.com/diamondburned/gotk4/pkg/gio/v2"
12 "github.com/diamondburned/gotk4/pkg/glib/v2"
13)
14
15// ContentBlockingManager handles WebKit content filtering
16type ContentBlockingManager struct {
17 filterStore *webkit.UserContentFilterStore
18 storagePath string
19
20 // Cache compiled filter to avoid recompilation
21 mu sync.RWMutex
22 compiledFilter *webkit.UserContentFilter
23 filterID string
24}
25
26// NewContentBlockingManager creates a new content blocking manager
27func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, error) {
28 // Create the filter store
29 filterStore := webkit.NewUserContentFilterStore(storagePath)
30 if filterStore == nil {
31 return nil, fmt.Errorf("failed to create UserContentFilterStore")
32 }
33
34 logging.Debug(fmt.Sprintf("[webkit] Created UserContentFilterStore at: %s", storagePath))
35
36 return &ContentBlockingManager{
37 filterStore: filterStore,
38 storagePath: storagePath,
39 }, nil
40}
41
42// CompileFilters compiles JSON filter rules and caches the result for reuse.
43// This should be called once during startup. Subsequent WebViews use ApplyCompiledFilter.
44func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules []byte, onComplete func(error)) {
45 if len(jsonRules) == 0 {
46 if onComplete != nil {
47 onComplete(fmt.Errorf("empty filter rules"))
48 }
49 return
50 }
51
52 logging.Debug(fmt.Sprintf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
53
54 // Store identifier for later use
55 cbm.mu.Lock()
56 cbm.filterID = identifier
57 cbm.mu.Unlock()
58
59 // Convert []byte to *glib.Bytes for WebKit API
60 gBytes := glib.NewBytesWithGo(jsonRules)
61
62 // Compile asynchronously - does not block main thread
63 ctx := context.Background()
64 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
65 filter, err := cbm.filterStore.SaveFinish(result)
66 if err != nil {
67 logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
68 if onComplete != nil {
69 onComplete(fmt.Errorf("failed to compile filter: %w", err))
70 }
71 return
72 }
73
74 if filter == nil {
75 logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
76 if onComplete != nil {
77 onComplete(fmt.Errorf("filter compilation returned nil"))
78 }
79 return
80 }
81
82 // Cache the compiled filter
83 cbm.mu.Lock()
84 cbm.compiledFilter = filter
85 cbm.mu.Unlock()
86
87 logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
88 if onComplete != nil {
89 onComplete(nil)
90 }
91 })
92}
93
94// ApplyCompiledFilter applies the pre-compiled filter to a UserContentManager.
95// This is instant since the filter is already compiled.
96func (cbm *ContentBlockingManager) ApplyCompiledFilter(ucm *webkit.UserContentManager) error {
97 if ucm == nil {
98 return fmt.Errorf("UserContentManager is nil")
99 }
100
101 cbm.mu.RLock()
102 filter := cbm.compiledFilter
103 cbm.mu.RUnlock()
104
105 if filter == nil {
106 return fmt.Errorf("no compiled filter available")
107 }
108
109 ucm.AddFilter(filter)
110 return nil
111}
112
113// IsFilterCompiled returns true if filters have been compiled and are ready to apply.
114func (cbm *ContentBlockingManager) IsFilterCompiled() bool {
115 cbm.mu.RLock()
116 defer cbm.mu.RUnlock()
117 return cbm.compiledFilter != nil
118}
119
120// ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager.
121// DEPRECATED: Use CompileFilters + ApplyCompiledFilter for better performance.
122// This method is kept for backward compatibility.
123func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error {
124 if ucm == nil {
125 return fmt.Errorf("UserContentManager is nil")
126 }
127
128 // Check if already compiled
129 cbm.mu.RLock()
130 if cbm.compiledFilter != nil {
131 filter := cbm.compiledFilter
132 cbm.mu.RUnlock()
133 ucm.AddFilter(filter)
134 logging.Debug(fmt.Sprintf("[webkit] Applied cached content filter to UCM"))
135 return nil
136 }
137 cbm.mu.RUnlock()
138
139 if len(jsonRules) == 0 {
140 return fmt.Errorf("empty filter rules")
141 }
142
143 logging.Debug(fmt.Sprintf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
144
145 // Convert []byte to *glib.Bytes for WebKit API
146 gBytes := glib.NewBytesWithGo(jsonRules)
147
148 ctx := context.Background()
149 done := make(chan error, 1)
150
151 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
152 filter, err := cbm.filterStore.SaveFinish(result)
153 if err != nil {
154 logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
155 done <- fmt.Errorf("failed to compile filter: %w", err)
156 return
157 }
158
159 if filter == nil {
160 logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
161 done <- fmt.Errorf("filter compilation returned nil")
162 return
163 }
164
165 // Cache for future use
166 cbm.mu.Lock()
167 cbm.compiledFilter = filter
168 cbm.filterID = identifier
169 cbm.mu.Unlock()
170
171 RunOnMainThread(func() {
172 ucm.AddFilter(filter)
173 logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
174 done <- nil
175 })
176 })
177
178 return <-done
179}
180
181// RemoveAllFilters removes all content filters from a UserContentManager
182func (cbm *ContentBlockingManager) RemoveAllFilters(ucm *webkit.UserContentManager) {
183 if ucm != nil {
184 ucm.RemoveAllFilters()
185 logging.Debug(fmt.Sprintf("[webkit] Removed all content filters"))
186 }
187}
188
189// GetFilterStore returns the underlying UserContentFilterStore
190func (cbm *ContentBlockingManager) GetFilterStore() *webkit.UserContentFilterStore {
191 return cbm.filterStore
192}
193
194// ApplyFiltersToWebView applies content blocking filters to a WebView
195func ApplyFiltersToWebView(view *WebView, filterJSON []byte) error {
196 if view == nil || view.view == nil {
197 return fmt.Errorf("invalid WebView")
198 }
199
200 // Get the UserContentManager
201 ucm := view.view.UserContentManager()
202 if ucm == nil {
203 return fmt.Errorf("UserContentManager is nil")
204 }
205
206 // Create content blocking manager
207 // Use a subdirectory of the cache dir for filter storage
208 dataDir := view.config.DataDir
209 if dataDir == "" {
210 return fmt.Errorf("WebView has no data directory configured")
211 }
212
213 filterStorePath := filepath.Join(dataDir, "content-filters")
214 cbm, err := NewContentBlockingManager(filterStorePath)
215 if err != nil {
216 return fmt.Errorf("failed to create content blocking manager: %w", err)
217 }
218
219 // Apply the filters
220 return cbm.ApplyFiltersFromJSON(ucm, "adblock-filters", filterJSON)
221}