1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4
7#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8#include <webkit/webkit.h>
9#include <stdlib.h>
10
11// Global filter store instance
12static WebKitUserContentFilterStore *filter_store = NULL;
13
14// External Go functions
15extern char* get_webkit_filter_store_path();
16extern void log_filter_success(char* identifier);
17extern void log_filter_error(char* error_msg, char* identifier);
18
19// Callback for save operation completion
20static void filter_save_callback(GObject *source_object,
21 GAsyncResult *result,
22 gpointer user_data) {
23 WebKitUserContentFilterStore *store = WEBKIT_USER_CONTENT_FILTER_STORE(source_object);
24 WebKitUserContentManager *manager = WEBKIT_USER_CONTENT_MANAGER(user_data);
25 GError *error = NULL;
26
27 WebKitUserContentFilter *filter = webkit_user_content_filter_store_save_finish(store, result, &error);
28
29 if (filter && !error) {
30 webkit_user_content_manager_add_filter(manager, filter);
31 const char *id = webkit_user_content_filter_get_identifier(filter);
32 log_filter_success((char*)id);
33 webkit_user_content_filter_unref(filter);
34 }
35
36 if (error) {
37 char *msg = g_strdup(error->message);
38 log_filter_error(msg, "unknown");
39 g_free(msg);
40 g_error_free(error);
41 }
42}
43
44// Initialize filter store if not already done
45static void ensure_filter_store() {
46 if (filter_store == NULL) {
47 // Get XDG-compliant path from Go (with tmp fallback)
48 char *store_path = get_webkit_filter_store_path();
49 filter_store = webkit_user_content_filter_store_new(store_path);
50 free(store_path); // Free the C string returned by Go
51 }
52}
53
54static void apply_content_filter(WebKitUserContentManager *manager,
55 const char *filter_json,
56 const char *identifier) {
57 ensure_filter_store();
58
59 GBytes *bytes = g_bytes_new(filter_json, strlen(filter_json));
60
61 // Save filter to store asynchronously - it will be applied in the callback
62 webkit_user_content_filter_store_save(filter_store,
63 identifier,
64 bytes,
65 NULL,
66 filter_save_callback,
67 manager);
68
69 g_bytes_unref(bytes);
70}
71
72static void inject_cosmetic_script(WebKitUserContentManager *manager,
73 const char *script_source) {
74 // Use isolated script world to prevent interference with page JavaScript
75 WebKitUserScript *script = webkit_user_script_new_for_world(
76 script_source,
77 WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
78 WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END, // Changed from DOCUMENT_START to avoid preconnect interference
79 "dumber-cosmetic", // Isolated world name
80 NULL, NULL
81 );
82
83 webkit_user_content_manager_add_script(manager, script);
84 webkit_user_script_unref(script);
85}
86
87static void clear_all_filters(WebKitUserContentManager *manager) {
88 webkit_user_content_manager_remove_all_filters(manager);
89}
90
91static void clear_all_scripts(WebKitUserContentManager *manager) {
92 webkit_user_content_manager_remove_all_scripts(manager);
93}
94*/
95import "C"
96
97import (
98 "encoding/json"
99 "fmt"
100 "os"
101 "path/filepath"
102 "unsafe"
103
104 "github.com/bnema/dumber/internal/config"
105 "github.com/bnema/dumber/internal/logging"
106)
107
108//export get_webkit_filter_store_path
109func get_webkit_filter_store_path() *C.char {
110 // Try XDG directories first
111 if filterCacheDir, err := config.GetFilterCacheDir(); err == nil {
112 webkitFilterDir := filepath.Join(filterCacheDir, "webkit-store")
113 if err := os.MkdirAll(webkitFilterDir, 0755); err == nil {
114 return C.CString(webkitFilterDir)
115 }
116 }
117
118 // Fallback to tmp if XDG fails
119 tmpDir := os.TempDir()
120 fallbackDir := filepath.Join(tmpDir, "dumber-filters")
121 if err := os.MkdirAll(fallbackDir, 0755); err == nil {
122 logging.Warn("[webkit] Using fallback tmp directory for filter store: " + fallbackDir)
123 return C.CString(fallbackDir)
124 }
125
126 // Last resort - return tmp dir
127 logging.Error("[webkit] Failed to create filter store directory, using tmp")
128 return C.CString(tmpDir)
129}
130
131//export log_filter_success
132func log_filter_success(identifier *C.char) {
133 id := C.GoString(identifier)
134 logging.Info("[webkit] Network filter applied successfully: " + id)
135}
136
137//export log_filter_error
138func log_filter_error(errorMsg *C.char, identifier *C.char) {
139 msg := C.GoString(errorMsg)
140 id := C.GoString(identifier)
141 logging.Error("[webkit] Failed to apply network filter '" + id + "': " + msg)
142}
143
144// ApplyContentFilters applies WebKit native content filters with error recovery
145func (w *WebView) ApplyContentFilters(filters []byte, identifier string) error {
146 // Check WebView state with debug support
147 if !w.CheckWebViewState() {
148 w.LogDebugInfo()
149 return ErrWebViewNotInitialized
150 }
151
152 // Validate input
153 if len(filters) == 0 || identifier == "" {
154 return fmt.Errorf("invalid filter parameters: empty filters or identifier")
155 }
156
157 // Check if filters are valid JSON
158 if !json.Valid(filters) {
159 return fmt.Errorf("invalid filter JSON for identifier: %s", identifier)
160 }
161
162 defer func() {
163 if r := recover(); r != nil {
164 logging.Error(fmt.Sprintf("[webkit] Panic in ApplyContentFilters: %v", r))
165 }
166 }()
167
168 manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
169 if manager == nil {
170 return ErrContentManagerNotFound
171 }
172
173 cFilters := C.CString(string(filters))
174 cID := C.CString(identifier)
175 defer C.free(unsafe.Pointer(cFilters))
176 defer C.free(unsafe.Pointer(cID))
177
178 logging.Info("[webkit] Applying network filters: " + identifier + " (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
179
180 // Apply filter with error checking
181 C.apply_content_filter(manager, cFilters, cID)
182 return nil
183}
184
185// InjectCosmeticFilter injects JavaScript for cosmetic filtering with error recovery
186func (w *WebView) InjectCosmeticFilter(script string) error {
187 // Check WebView state with debug support
188 if !w.CheckWebViewState() {
189 w.LogDebugInfo()
190 return ErrWebViewNotInitialized
191 }
192
193 // Validate input
194 if len(script) == 0 {
195 return fmt.Errorf("empty cosmetic filter script")
196 }
197
198 defer func() {
199 if r := recover(); r != nil {
200 logging.Error(fmt.Sprintf("[webkit] Panic in InjectCosmeticFilter: %v", r))
201 }
202 }()
203
204 manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
205 if manager == nil {
206 return ErrContentManagerNotFound
207 }
208
209 cScript := C.CString(script)
210 defer C.free(unsafe.Pointer(cScript))
211
212 logging.Info("[webkit] Injecting cosmetic filter script (" + fmt.Sprintf("%d", len(script)) + " chars)")
213
214 // Inject script with error recovery
215 C.inject_cosmetic_script(manager, cScript)
216 return nil
217}
218
219// ClearAllFilters removes all content filters
220func (w *WebView) ClearAllFilters() error {
221 if w.native == nil || w.native.wv == nil {
222 return ErrWebViewNotInitialized
223 }
224
225 manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
226 if manager == nil {
227 return ErrContentManagerNotFound
228 }
229
230 logging.Info("[webkit] Clearing all content filters")
231 C.clear_all_filters(manager)
232 return nil
233}
234
235// ClearAllScripts removes all injected scripts
236func (w *WebView) ClearAllScripts() error {
237 if w.native == nil || w.native.wv == nil {
238 return ErrWebViewNotInitialized
239 }
240
241 manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
242 if manager == nil {
243 return ErrContentManagerNotFound
244 }
245
246 logging.Info("[webkit] Clearing all injected scripts")
247 C.clear_all_scripts(manager)
248 return nil
249}