1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
7#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8#include <stdlib.h>
9#include <string.h>
10#include <gtk/gtk.h>
11#include <webkit/webkit.h>
12#include <glib-object.h>
13#include <gdk/gdk.h>
14#include <jsc/jsc.h>
15#include <glib.h>
16#include <gio/gio.h>
17#include "webview_callbacks.h"
18
19static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
20
21// Forward declaration
22extern void goQuitMainLoop();
23
24// Connect window close signal to quit main loop
25static void connect_destroy_quit(GtkWidget* w) {
26 if (!w) return;
27 g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL);
28}
29static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
30// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
31
32// Forward declare helpers used below
33static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
34
35// TLS error handling forward declarations
36extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
37static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
38 const char *failing_uri,
39 GTlsCertificate *certificate,
40 GTlsCertificateFlags errors,
41 gpointer user_data);
42
43// Handle create signal for _blank links - redirect to current window
44static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
45 WebKitNavigationAction* navigation_action,
46 gpointer user_data) {
47 (void)user_data;
48
49 // Get the request from navigation action
50 WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action);
51 if (request) {
52 const gchar* uri = webkit_uri_request_get_uri(request);
53 if (uri) {
54 // Load in current window instead of creating new one
55 webkit_web_view_load_uri(web_view, uri);
56 }
57 }
58
59 // Return NULL to prevent new window creation
60 return NULL;
61}
62static void connect_tls_error_handler(WebKitWebView* wv);
63
64
65// Dialog callback structure for modern AlertDialog async handling
66typedef struct {
67 int response;
68 gboolean completed;
69 GMainLoop *loop;
70} AlertDialogResponse;
71
72static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
73static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
74static char* extract_certificate_info(GTlsCertificate* certificate);
75
76// Setup video acceleration environment variables
77static void setup_video_acceleration(const char* driver_name, int enable_all, int legacy) {
78 if (legacy) {
79 // Legacy mode: force old VA-API plugins, disable modern ones
80 setenv("WEBKIT_GST_ENABLE_LEGACY_VAAPI", "1", 1);
81 // Don't set other env vars in legacy mode to avoid conflicts
82 return;
83 }
84
85 // Modern mode (default): use improved VA-API handling
86 if (driver_name && strlen(driver_name) > 0) {
87 if (strcmp(driver_name, "nvidia") == 0) {
88 // NVIDIA uses different env vars for hardware video acceleration
89 setenv("VDPAU_DRIVER", "nvidia", 1);
90 setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
91 } else if (strcmp(driver_name, "vdpau") == 0) {
92 // Explicit VDPAU backend (for NVIDIA via VA-API)
93 setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
94 } else {
95 // AMD/Intel use VA-API directly
96 setenv("LIBVA_DRIVER_NAME", driver_name, 1);
97 }
98 }
99
100 if (enable_all) {
101 setenv("GST_VAAPI_ALL_DRIVERS", "1", 1);
102 }
103}
104
105// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
106static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {
107 WebKitUserContentManager* u = webkit_user_content_manager_new();
108 if (!u) return NULL;
109 // WebKitGTK 6: create NetworkSession with data/cache directories
110 WebKitNetworkSession* sess = webkit_network_session_new(
111 data_dir ? data_dir : "",
112 cache_dir ? cache_dir : "");
113 if (!sess) { g_object_unref(u); return NULL; }
114 // Apply memory pressure settings to the session when provided
115 if (pressure_settings) {
116 // WebKitGTK 6.0 API: setter expects only the settings pointer
117 webkit_network_session_set_memory_pressure_settings(pressure_settings);
118 }
119 // Configure cookie persistence and policy (no third-party by default)
120 WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
121 if (cm) {
122 if (cookie_path && cookie_path[0]) {
123 webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
124 }
125 // 2 => no-third-party (default), 0 => always, 1 => never
126 maybe_set_cookie_policy(cm, 2);
127 }
128 // Persist credentials (HTTP auth, etc.)
129 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
130 // Configure TLS errors to emit signals instead of failing silently
131 webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
132 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
133 "user-content-manager", u,
134 "network-session", sess,
135 NULL));
136 // WebView holds refs to provided objects; drop our temporary refs
137 g_object_unref(sess);
138 // Set WebView background to black (easier on eyes when pages are loading)
139 GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
140 webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
141
142 // Handle create signal to redirect _blank links to current window
143 g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL);
144
145 // Connect TLS error handler
146 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
147
148 if (out_ucm) { *out_ucm = u; }
149 return w;
150}
151
152static gboolean gtk_prefers_dark() {
153 // Method 1: Check GNOME desktop interface color-scheme (primary method)
154 GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
155 if (desktop_settings) {
156 gchar* color_scheme = g_settings_get_string(desktop_settings, "color-scheme");
157 if (color_scheme) {
158 gboolean prefer_dark = (g_strcmp0(color_scheme, "prefer-dark") == 0);
159 g_free(color_scheme);
160 g_object_unref(desktop_settings);
161 if (prefer_dark) return TRUE;
162 }
163 g_object_unref(desktop_settings);
164 }
165
166 // Method 2: Check GTK theme name for dark variants
167 GtkSettings* settings = gtk_settings_get_default();
168 if (settings) {
169 gchar* theme_name = NULL;
170 g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
171 if (theme_name) {
172 gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
173 strstr(theme_name, "-Dark") != NULL);
174 g_free(theme_name);
175 if (is_dark) return TRUE;
176 }
177
178 // Method 3: Check gtk-application-prefer-dark-theme (fallback)
179 gboolean prefer = FALSE;
180 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
181 if (prefer) return TRUE;
182 }
183
184 return FALSE;
185}
186
187// Note: preferred color scheme is handled via a user script injection to support
188// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
189
190extern void goOnUcmMessage(unsigned long id, char* json);
191
192// Helper functions to connect signals using static callbacks
193static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
194 if (!widget) return;
195 g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
196}
197
198static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
199 if (!widget) return;
200 g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
201}
202
203static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
204 if (!widget) return;
205 g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
206}
207
208static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
209 if (!settings) return;
210 g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
211}
212
213static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
214 if (!context || !scheme) return;
215 webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
216}
217static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) {
218 if (!context || !scheme) return;
219 WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context);
220 if (!sm) return;
221
222 // Mark as secure to avoid mixed-content warnings when requested by https pages
223 webkit_security_manager_register_uri_scheme_as_secure(sm, scheme);
224 // Treat as local scheme
225 webkit_security_manager_register_uri_scheme_as_local(sm, scheme);
226 // Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme)
227 webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
228}
229
230// Script message handler wiring for WebKitGTK 6
231// Forward declaration for callback used in g_signal_connect_data
232// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
233static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
234// Uses WebKitScriptMessage and script-message-received::NAME signal.
235static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
236 (void)ucm;
237 if (!val) return;
238 gchar* s = jsc_value_to_string(val);
239 if (!s) return;
240 goOnUcmMessage((unsigned long)user_data, s);
241 g_free(s);
242}
243
244static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
245 if (!ucm || !name) return FALSE;
246 // WebKitGTK 6 signature: (ucm, name, world_name)
247 webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
248 gchar* signal = g_strdup_printf("script-message-received::%s", name);
249 if (!signal) return FALSE;
250 g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
251 g_free(signal);
252 return TRUE;
253}
254
255// Register a script message handler for a specific JavaScript world
256static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) {
257 if (!ucm || !name || !world_name) return FALSE;
258 webkit_user_content_manager_register_script_message_handler(ucm, name, world_name);
259 gchar* signal = g_strdup_printf("script-message-received::%s", name);
260 if (!signal) return FALSE;
261 g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
262 g_free(signal);
263 return TRUE;
264}
265
266// ----- Conditional helpers for settings across WebKit versions -----
267#ifndef WEBKIT_CHECK_VERSION
268#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
269#endif
270
271static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
272#if WEBKIT_CHECK_VERSION(2,40,0)
273 // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
274 if (!settings) return;
275 if (mode == 1) {
276 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
277 } else if (mode == 2) {
278 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
279 } else {
280 // Leave default policy (auto) without forcing a value to avoid referencing
281 // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
282 }
283#else
284 (void)settings; (void)mode;
285#endif
286}
287
288static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
289#if WEBKIT_CHECK_VERSION(2,36,0)
290 if (!settings) return;
291 webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
292#else
293 (void)settings; (void)enable;
294#endif
295}
296
297static void maybe_set_webgl(WebKitSettings* settings, int enable) {
298#if WEBKIT_CHECK_VERSION(2,20,0)
299 if (!settings) return;
300 webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
301#else
302 (void)settings; (void)enable;
303#endif
304}
305
306static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
307#if WEBKIT_CHECK_VERSION(2,20,0)
308 if (!settings) return;
309 webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
310#else
311 (void)settings; (void)enable;
312#endif
313}
314
315static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
316#if WEBKIT_CHECK_VERSION(2,20,0)
317 if (!settings) return;
318 webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
319#else
320 (void)settings; (void)require;
321#endif
322}
323
324// Enable two-finger swipe back/forward navigation gestures when supported
325static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
326#if WEBKIT_CHECK_VERSION(2,38,0)
327 if (!settings) return;
328 webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
329#else
330 (void)settings; (void)enable;
331#endif
332}
333
334// Set cookie accept policy with compile-time compatibility across WebKit versions
335static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
336 if (!cm) return;
337#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
338 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
339 if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
340 else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
341 webkit_cookie_manager_set_accept_policy(cm, p);
342#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
343 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
344 if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
345 else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
346 webkit_cookie_manager_set_accept_policy(cm, p);
347#else
348 (void)policy; // Not supported in this header version
349#endif
350}
351
352// Memory pressure settings helpers (version-guarded)
353static WebKitMemoryPressureSettings* create_memory_pressure_settings(
354 unsigned int memory_limit_mb,
355 double conservative_threshold,
356 double strict_threshold,
357 double kill_threshold,
358 double poll_interval) {
359 // If headers don't expose memory pressure APIs, return NULL to signal unsupported
360#if WEBKIT_CHECK_VERSION(2,44,0)
361 WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
362 if (!settings) return NULL;
363
364 if (memory_limit_mb > 0) {
365 webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
366 }
367
368 webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
369 webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
370
371 if (kill_threshold > 0) {
372 webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
373 }
374
375 webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
376
377 return settings;
378#else
379 (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
380 return NULL;
381#endif
382}
383
384// Cache model configuration
385static void maybe_set_cache_model(WebKitWebContext* context, int model) {
386 if (!context) return;
387 WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
388
389 switch (model) {
390 case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
391 case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
392 case 2:
393#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
394 cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
395#else
396 cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
397#endif
398 break;
399 }
400
401 webkit_web_context_set_cache_model(context, cache_model);
402}
403
404// Additional settings for memory optimization
405static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
406#if WEBKIT_CHECK_VERSION(2,6,0)
407 if (!settings) return;
408 webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
409#else
410 (void)settings; (void)enable;
411#endif
412}
413
414// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
415static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
416 (void)settings; (void)enable;
417}
418
419// Apply memory pressure settings to context if available
420static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
421 // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
422 (void)context; (void)settings;
423}
424
425// Trigger JavaScript GC if supported by headers
426static void maybe_collect_js(WebKitWebContext* context) {
427 // Disabled by default due to API availability variance across 6.0 builds.
428 (void)context;
429}
430
431// TLS error handling implementation
432static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
433 const char *failing_uri,
434 GTlsCertificate *certificate,
435 GTlsCertificateFlags errors,
436 gpointer user_data) {
437 (void)user_data;
438
439 // Extract host from URI for the Go callback
440 const char* host = failing_uri;
441 char* host_copy = NULL;
442
443 if (strstr(failing_uri, "://")) {
444 host = strstr(failing_uri, "://") + 3;
445 char* slash = strchr(host, '/');
446 if (slash) {
447 // Create a temporary null-terminated host string
448 size_t host_len = slash - host;
449 host_copy = malloc(host_len + 1);
450 if (host_copy) {
451 strncpy(host_copy, host, host_len);
452 host_copy[host_len] = '\0';
453 host = host_copy;
454 }
455 }
456 }
457
458 // Extract certificate information
459 char* cert_info = extract_certificate_info(certificate);
460
461 gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
462
463 // If user accepted, allow the certificate for this host
464 if (should_proceed && certificate) {
465 printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
466 fflush(stdout);
467 // Get the network session from the web view
468 WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
469 if (session) {
470 webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
471 printf("[dumber] Certificate exception added for host: %s\n", host);
472 fflush(stdout);
473
474 // Try loading the URL again after adding the exception
475 webkit_web_view_load_uri(web_view, failing_uri);
476 printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
477 fflush(stdout);
478 }
479 }
480
481 // Free the certificate info string
482 if (cert_info) {
483 g_free(cert_info);
484 }
485
486 if (host_copy) {
487 free(host_copy);
488 }
489
490 return should_proceed;
491}
492
493// Extract certificate information for display
494static char* extract_certificate_info(GTlsCertificate* certificate) {
495 if (!certificate) {
496 return g_strdup("Certificate information not available");
497 }
498
499 printf("[dumber] Extracting certificate information...\n");
500 fflush(stdout);
501
502 GString* info = g_string_new("Certificate Information:\n");
503
504 // Try to get certificate properties - but these might not be available in all GTK versions
505 gchar* subject = NULL;
506 gchar* issuer = NULL;
507 GDateTime* not_valid_before = NULL;
508 GDateTime* not_valid_after = NULL;
509
510 // Try property access but handle failures gracefully
511 g_object_get(certificate,
512 "subject-name", &subject,
513 "issuer-name", &issuer,
514 "not-valid-before", ¬_valid_before,
515 "not-valid-after", ¬_valid_after,
516 NULL);
517
518 if (subject) {
519 g_string_append_printf(info, "Subject: %s\n", subject);
520 g_free(subject);
521 printf("[dumber] Found subject info\n");
522 } else {
523 g_string_append(info, "Subject: Information not available\n");
524 }
525
526 if (issuer) {
527 g_string_append_printf(info, "Issued by: %s\n", issuer);
528 g_free(issuer);
529 printf("[dumber] Found issuer info\n");
530 } else {
531 g_string_append(info, "Issued by: Information not available\n");
532 }
533
534 if (not_valid_before) {
535 gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
536 g_string_append_printf(info, "Valid from: %s\n", date_str);
537 g_free(date_str);
538 g_date_time_unref(not_valid_before);
539 } else {
540 g_string_append(info, "Valid from: Information not available\n");
541 }
542
543 if (not_valid_after) {
544 gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
545 g_string_append_printf(info, "Valid until: %s\n", date_str);
546 g_free(date_str);
547 g_date_time_unref(not_valid_after);
548 } else {
549 g_string_append(info, "Valid until: Information not available\n");
550 }
551
552 char* result = g_string_free(info, FALSE);
553 printf("[dumber] Certificate info: %s\n", result);
554 fflush(stdout);
555 return result;
556}
557
558static void connect_tls_error_handler(WebKitWebView* wv) {
559 if (!wv) return;
560 g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
561 G_CALLBACK(on_load_failed_with_tls_errors), NULL);
562}
563
564// Modern AlertDialog async callback
565static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
566 AlertDialogResponse *data = (AlertDialogResponse*)user_data;
567 GError *error = NULL;
568 data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
569 if (error) {
570 printf("[dumber] AlertDialog error: %s\n", error->message);
571 g_error_free(error);
572 data->response = 0;
573 }
574 data->completed = TRUE;
575 const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
576 const char *response_name = (data->response >= 0 && data->response <= 2) ?
577 response_names[data->response] : "UNKNOWN";
578 printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
579 fflush(stdout);
580 if (data->loop) {
581 g_main_loop_quit(data->loop);
582 }
583}
584
585// Show TLS warning dialog synchronously using modern GtkAlertDialog API
586static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
587 printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
588 printf("[dumber] Error message: %s\n", error_msg);
589 fflush(stdout);
590
591 // Create the modern AlertDialog
592 GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
593
594 // Set modal behavior and error details
595 gtk_alert_dialog_set_modal(dialog, TRUE);
596 gtk_alert_dialog_set_detail(dialog, error_msg);
597
598 // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
599 const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
600 gtk_alert_dialog_set_buttons(dialog, buttons);
601 gtk_alert_dialog_set_default_button(dialog, 0);
602 gtk_alert_dialog_set_cancel_button(dialog, 0);
603
604 // Set up response data with main loop for sync behavior
605 AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
606
607 // Start async operation
608 gtk_alert_dialog_choose(dialog, parent, NULL,
609 on_alert_dialog_choose_done, &response_data);
610
611 // Run event loop until dialog completes
612 g_main_loop_run(response_data.loop);
613 g_main_loop_unref(response_data.loop);
614
615 // Clean up
616 g_object_unref(dialog);
617
618 // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
619 return response_data.response;
620}
621*/
622import "C"
623
624import (
625 "context"
626 "crypto/sha256"
627 "database/sql"
628 "errors"
629 "fmt"
630 "log"
631 "os"
632 "path/filepath"
633 "runtime"
634 "strings"
635 "time"
636 "unsafe"
637
638 "github.com/bnema/dumber/internal/config"
639 "github.com/bnema/dumber/internal/db"
640 "github.com/bnema/dumber/internal/logging"
641 _ "github.com/ncruces/go-sqlite3"
642)
643
644// FilterManager interface for content blocking integration
645type FilterManager interface {
646 GetNetworkFilters() ([]byte, error)
647 GetCosmeticScript() string
648 GetCosmeticScriptForDomain(domain string) string
649}
650
651// Helper function to convert bool to int for C interop
652func boolToInt(b bool) int {
653 if b {
654 return 1
655 }
656 return 0
657}
658
659// getCertificateHash generates a SHA256 hash of the certificate info for storage
660func getCertificateHash(certificateInfo string) string {
661 hash := sha256.Sum256([]byte(certificateInfo))
662 return fmt.Sprintf("%x", hash)
663}
664
665// getDBConnection opens a database connection and returns queries
666func getDBConnection() (*db.Queries, *sql.DB, error) {
667 cfg := config.Get()
668 if cfg == nil {
669 return nil, nil, fmt.Errorf("config not available")
670 }
671
672 dbPath := cfg.Database.Path
673 if dbPath == "" {
674 var err error
675 dbPath, err = config.GetDatabaseFile()
676 if err != nil {
677 return nil, nil, fmt.Errorf("failed to get database path: %w", err)
678 }
679 }
680
681 database, err := sql.Open("sqlite3", dbPath)
682 if err != nil {
683 return nil, nil, fmt.Errorf("failed to open database: %w", err)
684 }
685
686 queries := db.New(database)
687 return queries, database, nil
688}
689
690// checkStoredCertificateDecision checks if user has previously decided on this certificate
691func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
692 queries, database, err := getDBConnection()
693 if err != nil {
694 log.Printf("[tls] Failed to connect to database: %v", err)
695 return "", false
696 }
697 defer database.Close()
698
699 certHash := getCertificateHash(certificateInfo)
700 ctx := context.Background()
701
702 validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
703 if err != nil {
704 if err == sql.ErrNoRows {
705 return "", false // No previous decision
706 }
707 log.Printf("[tls] Failed to query certificate validation: %v", err)
708 return "", false
709 }
710
711 log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
712 return validation.UserDecision, true
713}
714
715// storeCertificateDecision stores the user's decision about a certificate
716func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
717 queries, database, err := getDBConnection()
718 if err != nil {
719 return fmt.Errorf("failed to connect to database: %w", err)
720 }
721 defer database.Close()
722
723 certHash := getCertificateHash(certificateInfo)
724 ctx := context.Background()
725
726 var expiresAt sql.NullTime
727 if !permanent {
728 // For temporary decisions, expire after 24 hours
729 expiresAt = sql.NullTime{
730 Time: time.Now().Add(24 * time.Hour),
731 Valid: true,
732 }
733 }
734
735 err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
736 if err != nil {
737 return fmt.Errorf("failed to store certificate validation: %w", err)
738 }
739
740 log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
741 return nil
742}
743
744type nativeView struct {
745 win *C.GtkWidget
746 view *C.GtkWidget
747 wv *C.WebKitWebView
748 ucm *C.WebKitUserContentManager
749}
750
751type memoryStats struct {
752 pageLoadCount int
753 lastGCTime time.Time
754 memoryPressureSettings *C.WebKitMemoryPressureSettings
755}
756
757// WebView represents a browser view powered by WebKit2GTK.
758type WebView struct {
759 config *Config
760 zoom float64
761 url string
762 destroyed bool
763 native *nativeView
764 window *Window
765 id uintptr
766 msgHandler func(payload string)
767 titleHandler func(title string)
768 uriHandler func(uri string)
769 zoomHandler func(level float64)
770 memStats *memoryStats
771 gcTicker *time.Ticker
772 gcDone chan struct{}
773 tlsExceptions map[string]bool // host -> whether user allowed certificate exception
774}
775
776// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
777func NewWebView(cfg *Config) (*WebView, error) {
778 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
779 if C.gtk_init_check() == 0 {
780 return nil, errors.New("failed to initialize GTK")
781 }
782
783 if cfg == nil {
784 cfg = &Config{}
785 }
786
787 // Setup video acceleration environment variables before creating WebView
788 if cfg.VideoAcceleration.EnableVAAPI {
789 driverName := C.CString(cfg.VideoAcceleration.VAAPIDriverName)
790 enableAll := 0
791 if cfg.VideoAcceleration.EnableAllDrivers {
792 enableAll = 1
793 }
794 legacy := 0
795 if cfg.VideoAcceleration.LegacyVAAPI {
796 legacy = 1
797 }
798 C.setup_video_acceleration(driverName, C.int(enableAll), C.int(legacy))
799 C.free(unsafe.Pointer(driverName))
800 }
801
802 // Prepare persistent website data/caches
803 dataDir := cfg.DataDir
804 cacheDir := cfg.CacheDir
805 if dataDir == "" {
806 dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
807 }
808 if cacheDir == "" {
809 cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
810 }
811 _ = os.MkdirAll(dataDir, 0o755)
812 _ = os.MkdirAll(cacheDir, 0o755)
813
814 cData := C.CString(dataDir)
815 cCache := C.CString(cacheDir)
816 cookieFile := filepath.Join(dataDir, "cookies.db")
817 cCookie := C.CString(cookieFile)
818 defer C.free(unsafe.Pointer(cData))
819 defer C.free(unsafe.Pointer(cCache))
820 defer C.free(unsafe.Pointer(cCookie))
821 // Use default WebContext to register the custom URI scheme handler; the
822 // WebView itself is created with a WebKitNetworkSession for persistence.
823 ctx := C.webkit_web_context_get_default()
824
825 // Apply cache model to WebContext
826 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
827 cacheModel := 1
828 if cfg.Memory.CacheModel != "" {
829 switch cfg.Memory.CacheModel {
830 case CacheModelWebBrowser:
831 cacheModel = 1
832 case CacheModelPrimaryWebBrowser:
833 cacheModel = 2
834 default: // CacheModelDocumentViewer or unset
835 cacheModel = 0
836 }
837 }
838 C.maybe_set_cache_model(ctx, C.int(cacheModel))
839
840 // Configure memory pressure settings if specified
841 var pressureSettings *C.WebKitMemoryPressureSettings
842 // Only configure memory pressure if explicitly requested via config
843 if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
844 memLimitMB := cfg.Memory.MemoryLimitMB
845 conservativeThreshold := cfg.Memory.ConservativeThreshold
846 strictThreshold := cfg.Memory.StrictThreshold
847 killThreshold := cfg.Memory.KillThreshold
848 pollInterval := cfg.Memory.PollIntervalSeconds
849
850 pressureSettings = C.create_memory_pressure_settings(
851 C.uint(memLimitMB),
852 C.double(conservativeThreshold),
853 C.double(strictThreshold),
854 C.double(killThreshold),
855 C.double(pollInterval))
856
857 if pressureSettings != nil {
858 C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
859 if cfg.Memory.EnableMemoryMonitoring {
860 log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
861 memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
862 }
863 }
864 }
865
866 // Register custom URI scheme handler for "dumb://"
867 {
868 sch := C.CString("dumb")
869 C.register_uri_scheme_handler(ctx, sch)
870 C.register_uri_scheme_security(ctx, sch)
871 C.free(unsafe.Pointer(sch))
872 }
873 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
874
875 // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
876 var createdUcm *C.WebKitUserContentManager
877 viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
878 if viewWidget == nil {
879 return nil, errors.New("failed to create WebKitWebView")
880 }
881
882 // Create a top-level window to host the view
883 win := C.new_window()
884 if win == nil {
885 return nil, errors.New("failed to create GtkWindow")
886 }
887
888 // Pack view widget into the window
889 // GTK4: containers removed; use gtk_window_set_child
890 C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
891 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
892 C.connect_destroy_quit(win)
893
894 v := &WebView{
895 config: cfg,
896 zoom: 1.0,
897 native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
898 window: &Window{win: win},
899 memStats: &memoryStats{
900 pageLoadCount: 0,
901 lastGCTime: time.Now(),
902 memoryPressureSettings: pressureSettings,
903 },
904 gcDone: make(chan struct{}),
905 tlsExceptions: make(map[string]bool),
906 }
907 // Assign an ID for accelerator dispatch
908 v.id = nextViewID()
909 registerView(v.id, v)
910
911 // Register with global memory manager if monitoring is enabled
912 if cfg.Memory.EnableMemoryMonitoring {
913 if globalMemoryManager == nil {
914 InitializeGlobalMemoryManager(
915 cfg.Memory.EnableMemoryMonitoring,
916 cfg.Memory.ProcessRecycleThreshold,
917 30*time.Second, // Default monitoring interval
918 )
919 }
920 if globalMemoryManager != nil {
921 globalMemoryManager.RegisterWebView(v)
922 }
923 }
924 // Attach GTK4 input controllers (keyboard, mouse)
925 AttachKeyboardControllers(v)
926
927 // Watch for GTK theme changes and propagate to page at runtime
928 if settings := C.gtk_settings_get_default(); settings != nil {
929 C.connect_theme_changed_with_id(settings, C.ulong(v.id))
930 }
931
932 // Native zoom shortcuts (independent of app services)
933 // Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
934 _ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
935 nz := v.zoom
936 if nz <= 0 {
937 nz = 1.0
938 }
939 nz *= 1.1
940 if nz < 0.25 {
941 nz = 0.25
942 }
943 if nz > 5.0 {
944 nz = 5.0
945 }
946 _ = v.SetZoom(nz)
947 })
948 _ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
949 nz := v.zoom
950 if nz <= 0 {
951 nz = 1.0
952 }
953 nz *= 1.1
954 if nz < 0.25 {
955 nz = 0.25
956 }
957 if nz > 5.0 {
958 nz = 5.0
959 }
960 _ = v.SetZoom(nz)
961 })
962 _ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
963 nz := v.zoom
964 if nz <= 0 {
965 nz = 1.0
966 }
967 nz /= 1.1
968 if nz < 0.25 {
969 nz = 0.25
970 }
971 if nz > 5.0 {
972 nz = 5.0
973 }
974 _ = v.SetZoom(nz)
975 })
976 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
977 // Find in page (Ctrl/Cmd+F): use new keyboard service bridge
978 _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() {
979 _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))")
980 })
981 // Navigation with Alt+Arrow keys: use new keyboard service bridge
982 _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() {
983 _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))")
984 })
985 _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() {
986 _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))")
987 })
988 // Update window title when page title changes
989 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
990 // Also dispatch title change to Go with view id
991 C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
992 // Notify URI changes to Go to keep current URL in sync
993 C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
994 // Apply hardware acceleration and related settings based on cfg.Rendering
995 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
996 // Hardware acceleration policy (guarded by version)
997 switch cfg.Rendering.Mode {
998 case "gpu":
999 C.maybe_set_hw_policy(settings, 1)
1000 C.maybe_set_webgl(settings, 1)
1001 C.maybe_set_canvas_accel(settings, 1)
1002 case "cpu":
1003 C.maybe_set_hw_policy(settings, 2)
1004 C.maybe_set_webgl(settings, 0)
1005 C.maybe_set_canvas_accel(settings, 0)
1006 default: // auto
1007 C.maybe_set_hw_policy(settings, 0)
1008 C.maybe_set_webgl(settings, 1)
1009 C.maybe_set_canvas_accel(settings, 1)
1010 }
1011 // Optional compositing indicators for debugging (guarded by version)
1012 if cfg.Rendering.DebugGPU {
1013 C.maybe_set_draw_indicators(settings, 1)
1014 }
1015 // Reduce media pipeline churn by requiring a user gesture for playback
1016 C.maybe_set_media_user_gesture(settings, 1)
1017 // Enable trackpad back/forward gestures when available
1018 C.maybe_set_back_forward_gestures(settings, 1)
1019
1020 // Keep page cache enabled by default for performance
1021 enablePageCache := true
1022 if cfg.Memory.EnablePageCache {
1023 enablePageCache = true
1024 }
1025 C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
1026
1027 // Offline app cache is deprecated; keep as no-op
1028 C.maybe_set_offline_app_cache(settings, C.int(0))
1029
1030 if cfg.Memory.EnableMemoryMonitoring {
1031 log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
1032 }
1033 }
1034
1035 if cfg.ZoomDefault > 0 {
1036 v.zoom = cfg.ZoomDefault
1037 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
1038 }
1039 if cfg.EnableDeveloperExtras {
1040 settings := C.webkit_web_view_get_settings(v.native.wv)
1041 if settings != nil {
1042 C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
1043 }
1044 }
1045
1046 // Apply custom User-Agent for codec negotiation
1047 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1048 if cfg.CodecPreferences.CustomUserAgent != "" {
1049 cUserAgent := C.CString(cfg.CodecPreferences.CustomUserAgent)
1050 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cUserAgent))
1051 C.free(unsafe.Pointer(cUserAgent))
1052 log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
1053 } else if cfg.CodecPreferences.ForceAV1 {
1054 // Use modern Chrome UA that signals AV1 support
1055 av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1056 cAV1UA := C.CString(av1UA)
1057 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
1058 C.free(unsafe.Pointer(cAV1UA))
1059 log.Printf("[webkit] Set AV1-optimized User-Agent")
1060 }
1061 }
1062
1063 // Apply default fonts if provided in cfg
1064 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1065 if cfg.DefaultSansFont != "" {
1066 csans := C.CString(cfg.DefaultSansFont)
1067 C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
1068 C.free(unsafe.Pointer(csans))
1069 // Also set default font family to sans
1070 csans2 := C.CString(cfg.DefaultSansFont)
1071 C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
1072 C.free(unsafe.Pointer(csans2))
1073 }
1074 if cfg.DefaultSerifFont != "" {
1075 cserif := C.CString(cfg.DefaultSerifFont)
1076 C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
1077 C.free(unsafe.Pointer(cserif))
1078 }
1079 if cfg.DefaultMonospaceFont != "" {
1080 cmono := C.CString(cfg.DefaultMonospaceFont)
1081 C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
1082 C.free(unsafe.Pointer(cmono))
1083 }
1084 if cfg.DefaultFontSize > 0 {
1085 C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
1086 }
1087 }
1088 if cfg.InitialURL != "" {
1089 _ = v.LoadURL(cfg.InitialURL)
1090 }
1091 // Ensure native resources are released if user forgets to call Destroy
1092 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
1093 log.Printf("[webkit] WebView created (CGO)")
1094
1095 // Initialize UCM scripts and handlers
1096 if v.native != nil && v.native.ucm != nil {
1097 v.enableUserContentManager(cfg)
1098 }
1099
1100 // Start periodic JavaScript garbage collection if configured
1101 if cfg.Memory.EnableGCInterval > 0 {
1102 v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
1103 if cfg.Memory.EnableMemoryMonitoring {
1104 log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
1105 }
1106 }
1107
1108 return v, nil
1109}
1110
1111func (w *WebView) LoadURL(rawURL string) error {
1112 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1113 return ErrNotImplemented
1114 }
1115
1116 // Track page loads for process recycling
1117 if w.memStats != nil {
1118 w.memStats.pageLoadCount++
1119
1120 // Check if we need to recycle this WebView
1121 if w.config.Memory.ProcessRecycleThreshold > 0 &&
1122 w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
1123 if w.config.Memory.EnableMemoryMonitoring {
1124 log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
1125 w.memStats.pageLoadCount)
1126 }
1127 }
1128 }
1129
1130 w.url = rawURL
1131 curl := C.CString(rawURL)
1132 defer C.free(unsafe.Pointer(curl))
1133 C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
1134 log.Printf("[webkit] LoadURL: %s", rawURL)
1135 return nil
1136}
1137
1138func (w *WebView) Show() error {
1139 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
1140 return ErrNotImplemented
1141 }
1142 C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
1143 C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
1144 C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1145 log.Printf("[webkit] Show window")
1146 return nil
1147}
1148
1149func (w *WebView) Hide() error {
1150 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
1151 return ErrNotImplemented
1152 }
1153 C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
1154 C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
1155 return nil
1156}
1157
1158func (w *WebView) Destroy() error {
1159 if w == nil || w.native == nil || w.native.win == nil {
1160 return ErrNotImplemented
1161 }
1162
1163 // Stop periodic GC if running
1164 w.stopPeriodicGC()
1165
1166 // Clean up memory pressure settings
1167 if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
1168 // WebKit will clean up the memory pressure settings when context is destroyed
1169 w.memStats.memoryPressureSettings = nil
1170 }
1171
1172 // Unregister from memory manager
1173 if globalMemoryManager != nil {
1174 globalMemoryManager.UnregisterWebView(w.id)
1175 }
1176
1177 // GTK4: destroy windows via gtk_window_destroy
1178 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1179 w.destroyed = true
1180 unregisterView(w.id)
1181 log.Printf("[webkit] Destroy window")
1182 return nil
1183}
1184
1185// Window returns the associated native window.
1186func (w *WebView) Window() *Window { return w.window }
1187
1188// GetCurrentURL returns the current URI from WebKit.
1189func (w *WebView) GetCurrentURL() string {
1190 if w == nil || w.native == nil || w.native.wv == nil {
1191 return ""
1192 }
1193 uri := C.webkit_web_view_get_uri(w.native.wv)
1194 if uri == nil {
1195 return ""
1196 }
1197 return C.GoString((*C.char)(unsafe.Pointer(uri)))
1198}
1199
1200func (w *WebView) GoBack() error {
1201 if w == nil || w.native == nil || w.native.wv == nil {
1202 return ErrNotImplemented
1203 }
1204 C.webkit_web_view_go_back(w.native.wv)
1205 return nil
1206}
1207
1208func (w *WebView) GoForward() error {
1209 if w == nil || w.native == nil || w.native.wv == nil {
1210 return ErrNotImplemented
1211 }
1212 C.webkit_web_view_go_forward(w.native.wv)
1213 return nil
1214}
1215
1216func (w *WebView) Reload() error {
1217 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1218 return ErrNotImplemented
1219 }
1220 C.webkit_web_view_reload(w.native.wv)
1221 log.Printf("[webkit] Reload page")
1222 return nil
1223}
1224
1225func (w *WebView) ReloadBypassCache() error {
1226 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1227 return ErrNotImplemented
1228 }
1229 C.webkit_web_view_reload_bypass_cache(w.native.wv)
1230 log.Printf("[webkit] Hard reload page (bypass cache)")
1231 return nil
1232}
1233
1234// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1235func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1236
1237func (w *WebView) dispatchScriptMessage(payload string) {
1238 if w != nil && w.msgHandler != nil {
1239 w.msgHandler(payload)
1240 }
1241}
1242
1243// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
1244func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
1245
1246func (w *WebView) dispatchTitleChanged(title string) {
1247 if w != nil && w.titleHandler != nil {
1248 w.titleHandler(title)
1249 }
1250}
1251
1252// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
1253func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
1254
1255func (w *WebView) dispatchURIChanged(uri string) {
1256 if w != nil && w.uriHandler != nil {
1257 w.uriHandler(uri)
1258 }
1259}
1260
1261// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
1262func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
1263
1264func (w *WebView) dispatchZoomChanged(level float64) {
1265 if w != nil && w.zoomHandler != nil {
1266 w.zoomHandler(level)
1267 }
1268}
1269
1270// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1271func (w *WebView) enableUserContentManager(cfg *Config) {
1272 if w == nil || w.native == nil || w.native.ucm == nil {
1273 return
1274 }
1275 // Register handler "dumber" for main world
1276 cname := C.CString("dumber")
1277 ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
1278 if ok == 0 {
1279 log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
1280 }
1281 C.free(unsafe.Pointer(cname))
1282
1283 // Also register handler for the isolated GUI world so GUI can post messages to native
1284 cworld := C.CString("dumber-gui")
1285 cname2 := C.CString("dumber")
1286 ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id))
1287 if ok2 == 0 {
1288 log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'")
1289 }
1290 C.free(unsafe.Pointer(cworld))
1291 C.free(unsafe.Pointer(cname2))
1292
1293 // Inject color-scheme preference script at document-start to inform sites of system theme
1294 preferDark := C.gtk_prefers_dark() != 0
1295 if preferDark {
1296 log.Printf("[theme] GTK prefers: dark")
1297 } else {
1298 log.Printf("[theme] GTK prefers: light")
1299 }
1300 // Load color scheme script from compiled GUI assets instead of inline
1301 var schemeJS string
1302 if cfg != nil && cfg.Assets != nil {
1303 if schemeBytes, err := cfg.Assets.ReadFile("assets/gui/color-scheme.js"); err == nil {
1304 baseScript := string(schemeBytes)
1305 // Inject the GTK preference as a parameter
1306 if preferDark {
1307 schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = true; %s", baseScript)
1308 } else {
1309 schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = false; %s", baseScript)
1310 }
1311 } else {
1312 log.Printf("[webkit] Warning: Failed to load color-scheme.js from assets, falling back to basic theme detection: %v", err)
1313 // Simple fallback if the compiled script is not available
1314 if preferDark {
1315 schemeJS = `console.log('[dumber] GTK detected color mode: dark'); document.documentElement.classList.add('dark');`
1316 } else {
1317 schemeJS = `console.log('[dumber] GTK detected color mode: light'); document.documentElement.classList.remove('dark');`
1318 }
1319 }
1320 }
1321 cScheme := C.CString(schemeJS)
1322 defer C.free(unsafe.Pointer(cScheme))
1323 schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1324 if schemeScript != nil {
1325 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
1326 C.webkit_user_script_unref(schemeScript)
1327 }
1328
1329 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
1330 if cfg != nil && cfg.Assets != nil {
1331 log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
1332 if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
1333 guiScript := string(guiBytes)
1334 log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
1335
1336 cGui := C.CString(guiScript)
1337 defer C.free(unsafe.Pointer(cGui))
1338
1339 log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'")
1340 world := C.CString("dumber-gui")
1341 defer C.free(unsafe.Pointer(world))
1342 guiUserScript := C.webkit_user_script_new_for_world((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil)
1343 if guiUserScript != nil {
1344 C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
1345 C.webkit_user_script_unref(guiUserScript)
1346 log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world")
1347 } else {
1348 log.Printf("[webkit] ERROR: Failed to create GUI user script")
1349 }
1350
1351 // Inject a unified page-world bridge to dispatch standard events and expose unified API
1352 bridge := `(() => { try {
1353 if (window.__dumber_page_bridge_installed) return;
1354 window.__dumber_page_bridge_installed = true;
1355
1356 // Theme setter function for GTK theme integration
1357 window.__dumber_setTheme = (theme) => {
1358 window.__dumber_initial_theme = theme;
1359 console.log('[dumber] Setting theme to:', theme);
1360 if (theme === 'dark') {
1361 document.documentElement.classList.add('dark');
1362 } else {
1363 document.documentElement.classList.remove('dark');
1364 }
1365 };
1366
1367 // Ensure unified API object
1368 window.__dumber = window.__dumber || {};
1369 // Toast namespace
1370 window.__dumber.toast = window.__dumber.toast || {
1371 show: function(message, duration, type) {
1372 try {
1373 document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } }));
1374 // Legacy compatibility
1375 document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } }));
1376 } catch(e) { /* ignore */ }
1377 },
1378 zoom: function(level) {
1379 try {
1380 document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } }));
1381 // Legacy compatibility
1382 document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } }));
1383 } catch(e) { /* ignore */ }
1384 }
1385 };
1386 // Back-compat global helpers
1387 window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); };
1388 window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); };
1389
1390 // Omnibox suggestions bridge (with queue before ready)
1391 let __omniboxQueue = [];
1392 let __omniboxReady = false;
1393 function __omniboxDispatch(suggestions) {
1394 try {
1395 document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } }));
1396 // Legacy compatibility event name
1397 document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } }));
1398 } catch (e) { /* ignore */ }
1399 }
1400 window.__dumber_omnibox_suggestions = function(suggestions) {
1401 if (__omniboxReady) {
1402 __omniboxDispatch(suggestions);
1403 } else {
1404 try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ }
1405 }
1406 };
1407 document.addEventListener('dumber:omnibox-ready', function() {
1408 __omniboxReady = true;
1409 if (__omniboxQueue && __omniboxQueue.length) {
1410 const items = __omniboxQueue.slice();
1411 __omniboxQueue.length = 0;
1412 for (const s of items) __omniboxDispatch(s);
1413 }
1414 });
1415 // Unified omnibox API for page-world callers
1416 window.__dumber.omnibox = window.__dumber.omnibox || {
1417 suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); }
1418 };
1419 } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();`
1420 cBridge := C.CString(bridge)
1421 defer C.free(unsafe.Pointer(cBridge))
1422 bridgeScript := C.webkit_user_script_new((*C.gchar)(cBridge), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1423 if bridgeScript != nil {
1424 C.webkit_user_content_manager_add_script(w.native.ucm, bridgeScript)
1425 C.webkit_user_script_unref(bridgeScript)
1426 log.Printf("[webkit] Page-world toast bridge injected")
1427 }
1428
1429 // If an API token is provided, inject a fetch wrapper in the isolated GUI world
1430 if cfg.APIToken != "" {
1431 wrapper := "(() => { try {" +
1432 "const TOKEN='" + cfg.APIToken + "';" +
1433 "const appendToken=(u)=>{ try { let url; try { url = new URL(u); } catch(_) { url = new URL(u, 'dumb://homepage'); } if(url.protocol==='dumb:' && url.pathname.startsWith('/api')){ url.searchParams.set('token', TOKEN); return url.toString(); } } catch(_){} return u; };" +
1434 "const _fetch=window.fetch.bind(window); window.fetch=(input, init)=>{ try { if(typeof input==='string'){ input=appendToken(input); } else if(input && input.url){ const nu=appendToken(input.url); if(nu!==input.url){ input=new Request(nu, input); } } } catch(_){} return _fetch(input, init); };" +
1435 "} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();"
1436 cWrap := C.CString(wrapper)
1437 defer C.free(unsafe.Pointer(cWrap))
1438 world := C.CString("dumber-gui")
1439 defer C.free(unsafe.Pointer(world))
1440 wrapScript := C.webkit_user_script_new_for_world((*C.gchar)(cWrap), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil)
1441 if wrapScript != nil {
1442 C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript)
1443 C.webkit_user_script_unref(wrapScript)
1444 log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world")
1445 }
1446 }
1447 } else {
1448 log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
1449 }
1450
1451 // Add GUI CSS styles as user stylesheet using WebKit native API
1452 if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
1453 cssContent := string(cssBytes)
1454 cCss := C.CString(cssContent)
1455 defer C.free(unsafe.Pointer(cCss))
1456
1457 styleSheet := C.webkit_user_style_sheet_new(
1458 (*C.gchar)(cCss),
1459 C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1460 C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
1461 nil, // allow_list
1462 nil, // block_list
1463 )
1464 if styleSheet != nil {
1465 C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
1466 C.webkit_user_style_sheet_unref(styleSheet)
1467 } else {
1468 log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
1469 }
1470 } else {
1471 log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err)
1472 }
1473 }
1474
1475 // Inject Wails runtime fetch interceptor for homepage bridging
1476 wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`
1477 cWails := C.CString(wailsBridge)
1478 defer C.free(unsafe.Pointer(cWails))
1479 wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1480 if wailsScript != nil {
1481 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
1482 C.webkit_user_script_unref(wailsScript)
1483 }
1484
1485 // Inject codec control script if codec preferences are configured
1486 if cfg != nil && (cfg.CodecPreferences.ForceAV1 || len(cfg.CodecPreferences.BlockedCodecs) > 0 || len(cfg.CodecPreferences.PreferredCodecs) > 0) {
1487 codecScript := GenerateCodecControlScript(cfg.CodecPreferences)
1488 cCodec := C.CString(codecScript)
1489 defer C.free(unsafe.Pointer(cCodec))
1490 codecUserScript := C.webkit_user_script_new(
1491 (*C.gchar)(cCodec),
1492 C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
1493 C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
1494 nil, nil)
1495 if codecUserScript != nil {
1496 C.webkit_user_content_manager_add_script(w.native.ucm, codecUserScript)
1497 C.webkit_user_script_unref(codecUserScript)
1498 log.Printf("[webkit] Codec control script injected")
1499 }
1500 }
1501
1502 // No JS fallback bridge — native UCM is active
1503}
1504
1505// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
1506func (w *WebView) startPeriodicGC(interval time.Duration) {
1507 if w == nil || w.gcTicker != nil {
1508 return // Already running or invalid
1509 }
1510
1511 w.gcTicker = time.NewTicker(interval)
1512
1513 go func() {
1514 defer func() {
1515 if w.gcTicker != nil {
1516 w.gcTicker.Stop()
1517 w.gcTicker = nil
1518 }
1519 }()
1520
1521 for {
1522 select {
1523 case <-w.gcTicker.C:
1524 w.triggerGC()
1525 case <-w.gcDone:
1526 return
1527 }
1528 }
1529 }()
1530}
1531
1532// stopPeriodicGC stops the periodic garbage collection
1533func (w *WebView) stopPeriodicGC() {
1534 if w == nil {
1535 return
1536 }
1537
1538 if w.gcDone != nil {
1539 close(w.gcDone)
1540 w.gcDone = nil
1541 }
1542
1543 if w.gcTicker != nil {
1544 w.gcTicker.Stop()
1545 w.gcTicker = nil
1546 }
1547}
1548
1549// triggerGC manually triggers JavaScript garbage collection
1550func (w *WebView) triggerGC() {
1551 if w == nil || w.memStats == nil {
1552 return
1553 }
1554
1555 // Get default WebContext and trigger GC
1556 ctx := C.webkit_web_context_get_default()
1557 if ctx != nil {
1558 C.maybe_collect_js(ctx)
1559 w.memStats.lastGCTime = time.Now()
1560
1561 if w.config.Memory.EnableMemoryMonitoring {
1562 log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
1563 }
1564 }
1565}
1566
1567// GetMemoryStats returns current memory statistics for this WebView
1568func (w *WebView) GetMemoryStats() map[string]interface{} {
1569 if w == nil || w.memStats == nil {
1570 return nil
1571 }
1572
1573 return map[string]interface{}{
1574 "page_load_count": w.memStats.pageLoadCount,
1575 "last_gc_time": w.memStats.lastGCTime,
1576 "has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
1577 }
1578}
1579
1580// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
1581func (w *WebView) TriggerMemoryCleanup() {
1582 if w == nil {
1583 return
1584 }
1585
1586 w.triggerGC()
1587
1588 if w.config.Memory.EnableMemoryMonitoring {
1589 log.Printf("[webkit] Manual memory cleanup triggered")
1590 }
1591}
1592
1593//export goHandleTLSError
1594func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
1595 uri := C.GoString(failingURI)
1596 hostname := C.GoString(host)
1597 certificateInfo := C.GoString(certInfo)
1598
1599 log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
1600
1601 // Find the WebView that triggered this error
1602 // For now, we'll get the most recently created view
1603 // In a more sophisticated implementation, we'd pass the view ID through user_data
1604 var webView *WebView
1605 regMu.RLock()
1606 for _, v := range viewByID {
1607 if v != nil {
1608 webView = v
1609 break
1610 }
1611 }
1612 regMu.RUnlock()
1613
1614 if webView == nil {
1615 log.Printf("[tls] No WebView found to handle TLS error")
1616 return C.FALSE // Don't proceed
1617 }
1618
1619 // Check if we've already allowed this host
1620 if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
1621 log.Printf("[tls] Certificate exception already granted for %s", hostname)
1622 return C.TRUE // Proceed with the load
1623 }
1624
1625 // Show warning dialog and get user decision
1626 if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
1627 webView.tlsExceptions[hostname] = true
1628 log.Printf("[tls] User accepted certificate exception for %s", hostname)
1629 return C.TRUE // Proceed with the load
1630 }
1631
1632 log.Printf("[tls] User rejected certificate for %s", hostname)
1633 return C.FALSE // Don't proceed
1634}
1635
1636// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
1637func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1638 if w == nil || w.native == nil || w.native.win == nil {
1639 log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1640 return false
1641 }
1642
1643 // Check if we have a stored decision for this certificate
1644 if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
1645 switch storedDecision {
1646 case "accepted":
1647 log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
1648 return true
1649 case "rejected":
1650 log.Printf("[tls] Using stored REJECT decision for %s", hostname)
1651 return false
1652 default:
1653 log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
1654 }
1655 }
1656
1657 log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1658
1659 // Format the error message with certificate information
1660 errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
1661
1662 // Convert strings to C strings
1663 cHostname := C.CString(hostname)
1664 defer C.free(unsafe.Pointer(cHostname))
1665
1666 cErrorMsg := C.CString(errorMsg)
1667 defer C.free(unsafe.Pointer(cErrorMsg))
1668
1669 // Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
1670 result := C.show_tls_warning_dialog_sync(
1671 (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1672 cHostname,
1673 cErrorMsg,
1674 )
1675
1676 responseCode := int(result)
1677
1678 switch responseCode {
1679 case 0: // Go Back
1680 log.Printf("[tls] User chose to GO BACK for %s", hostname)
1681 return false
1682
1683 case 1: // Proceed Once
1684 log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
1685 // Store temporary decision (expires in 24 hours)
1686 if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
1687 log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
1688 }
1689 return true
1690
1691 case 2: // Always Accept
1692 log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
1693 // Store permanent decision
1694 if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
1695 log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
1696 }
1697 return true
1698
1699 default:
1700 log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
1701 return false
1702 }
1703}
1704
1705// formatTLSErrorMessage creates a detailed error message based on the error flags
1706func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
1707 msg := "Website: " + hostname + "\n\n"
1708
1709 // Add certificate information
1710 if certificateInfo != "" {
1711 msg += "Certificate Details:\n" + certificateInfo + "\n"
1712 }
1713
1714 msg += "Security Issues:\n"
1715 // Decode GTlsCertificateFlags (common values)
1716 if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
1717 msg += "• Certificate authority is not trusted\n"
1718 }
1719 if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
1720 msg += "• Certificate does not match the website identity\n"
1721 }
1722 if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
1723 msg += "• Certificate is not yet valid\n"
1724 }
1725 if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
1726 msg += "• Certificate has expired\n"
1727 }
1728 if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
1729 msg += "• Certificate has been revoked\n"
1730 }
1731 if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
1732 msg += "• Certificate uses weak cryptography\n"
1733 }
1734
1735 msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
1736
1737 return msg
1738}
1739
1740// InitializeContentBlocking initializes WebKit content blocking with filter manager
1741func (w *WebView) InitializeContentBlocking(filterManager FilterManager) error {
1742 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1743 return ErrWebViewNotInitialized
1744 }
1745
1746 // Load filters asynchronously with proper error recovery
1747 go func() {
1748 defer func() {
1749 if r := recover(); r != nil {
1750 logging.Error(fmt.Sprintf("[webkit] Content blocking initialization panic: %v", r))
1751 }
1752 }()
1753
1754 // Wait longer for WebView to be ready before applying filters on first load
1755 // This prevents interference with WebKit's preconnect operations
1756 time.Sleep(1500 * time.Millisecond)
1757
1758 // Retry mechanism with exponential backoff
1759 maxRetries := 5
1760 baseDelay := 200 * time.Millisecond
1761
1762 // Apply network filters with error recovery
1763 for i := 0; i < maxRetries; i++ {
1764 if w.destroyed {
1765 logging.Debug("[webkit] WebView destroyed, aborting filter loading")
1766 return
1767 }
1768
1769 if filters, err := filterManager.GetNetworkFilters(); err == nil {
1770 if len(filters) > 10 && string(filters) != "null" {
1771 logging.Info("[webkit] Loading network filters from filter manager (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
1772 if err := w.ApplyContentFilters(filters, "network-filters"); err != nil {
1773 logging.Error("Failed to apply network filters (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
1774 if i == maxRetries-1 {
1775 logging.Warn("[webkit] Network filtering disabled due to errors")
1776 break
1777 }
1778 time.Sleep(baseDelay * time.Duration(1<<uint(i))) // Exponential backoff
1779 continue
1780 }
1781 break
1782 }
1783 } else if i == maxRetries-1 {
1784 logging.Error("Failed to get network filters after retries: " + err.Error())
1785 }
1786 }
1787
1788 // Apply cosmetic filtering with error recovery
1789 time.Sleep(200 * time.Millisecond) // Additional delay to ensure network setup is complete
1790
1791 for i := 0; i < maxRetries; i++ {
1792 if w.destroyed {
1793 logging.Debug("[webkit] WebView destroyed, aborting cosmetic filter loading")
1794 return
1795 }
1796
1797 script := filterManager.GetCosmeticScript()
1798 if len(script) > 0 {
1799 logging.Info("[webkit] Loading cosmetic script from filter manager (" + fmt.Sprintf("%d", len(script)) + " chars)")
1800 if err := w.InjectCosmeticFilter(script); err != nil {
1801 logging.Error("Failed to inject cosmetic filter (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
1802 if i == maxRetries-1 {
1803 logging.Warn("[webkit] Cosmetic filtering disabled due to errors")
1804 } else {
1805 time.Sleep(baseDelay * time.Duration(1<<uint(i)))
1806 continue
1807 }
1808 }
1809 break
1810 }
1811 }
1812
1813 logging.Info("Content blocking initialization completed")
1814 }()
1815
1816 return nil
1817}
1818
1819// OnNavigate sets up domain-specific cosmetic filtering on navigation
1820func (w *WebView) OnNavigate(url string, filterManager FilterManager) {
1821 if w == nil || w.destroyed {
1822 return
1823 }
1824
1825 domain := extractDomain(url)
1826 if domain == "" {
1827 return
1828 }
1829
1830 // Get domain-specific cosmetic rules
1831 script := filterManager.GetCosmeticScriptForDomain(domain)
1832 if script != "" {
1833 // Inject domain-specific cosmetic rules
1834 logging.Info("[webkit] Applying domain-specific cosmetic rules for: " + domain + " (" + fmt.Sprintf("%d", len(script)) + " chars)")
1835 w.InjectScript(fmt.Sprintf(
1836 "if (typeof window.__dumber_cosmetic_init === 'function') { %s }",
1837 script,
1838 ))
1839 } else {
1840 logging.Debug("[webkit] No domain-specific cosmetic rules found for: " + domain)
1841 }
1842}
1843
1844// UpdateContentFilters updates the content filters dynamically
1845func (w *WebView) UpdateContentFilters(filterManager FilterManager) error {
1846 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1847 return ErrWebViewNotInitialized
1848 }
1849
1850 // Clear existing filters first
1851 if err := w.ClearAllFilters(); err != nil {
1852 return fmt.Errorf("failed to clear existing filters: %w", err)
1853 }
1854
1855 // Apply new filters
1856 if filters, err := filterManager.GetNetworkFilters(); err == nil {
1857 logging.Info("[webkit] Updating network filters (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
1858 if err := w.ApplyContentFilters(filters, "network-filters-updated"); err != nil {
1859 return fmt.Errorf("failed to apply updated network filters: %w", err)
1860 }
1861 }
1862
1863 // Update cosmetic filtering
1864 script := filterManager.GetCosmeticScript()
1865 logging.Info("[webkit] Updating cosmetic script (" + fmt.Sprintf("%d", len(script)) + " chars)")
1866 if err := w.InjectCosmeticFilter(script); err != nil {
1867 return fmt.Errorf("failed to inject updated cosmetic filter: %w", err)
1868 }
1869
1870 logging.Info("Content filters updated successfully")
1871 return nil
1872}
1873
1874// extractDomain extracts the domain from a URL
1875func extractDomain(url string) string {
1876 if url == "" {
1877 return ""
1878 }
1879
1880 // Simple domain extraction - remove protocol and path
1881 start := strings.Index(url, "://")
1882 if start != -1 {
1883 url = url[start+3:]
1884 }
1885
1886 end := strings.Index(url, "/")
1887 if end != -1 {
1888 url = url[:end]
1889 }
1890
1891 // Remove port if present
1892 if portIdx := strings.Index(url, ":"); portIdx != -1 {
1893 url = url[:portIdx]
1894 }
1895
1896 return url
1897}