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 <stdint.h>
11#include <gtk/gtk.h>
12#include <webkit/webkit.h>
13#include <glib-object.h>
14#include <gdk/gdk.h>
15#include <jsc/jsc.h>
16#include <glib.h>
17#include <gio/gio.h>
18#include "webview_callbacks.h"
19
20static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
21
22// Forward declaration
23extern void goQuitMainLoop();
24extern void goInvokeHandle(uintptr_t handle);
25extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type, char* frame_name, gboolean is_user_gesture, guint modifiers, guint mouse_button);
26extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height);
27extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event);
28extern void goHandleWebViewClose(unsigned long id);
29extern void goCreateNewPanel(unsigned long id, char* uri);
30static gboolean invoke_handle_cb(gpointer data) {
31 goInvokeHandle((uintptr_t)data);
32 return G_SOURCE_REMOVE;
33}
34
35static void schedule_on_main_thread(uintptr_t handle) {
36 g_idle_add_full(G_PRIORITY_DEFAULT, invoke_handle_cb, (gpointer)handle, NULL);
37}
38
39// Connect window close signal to quit main loop
40static void connect_destroy_quit(GtkWidget* w) {
41 if (!w) return;
42 g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL);
43}
44static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
45// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
46
47// Forward declare helpers used below
48static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
49
50// TLS error handling forward declarations
51extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
52
53// Console message handling forward declarations
54extern void goHandleConsoleMessage(char* message);
55
56static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
57 const char *failing_uri,
58 GTlsCertificate *certificate,
59 GTlsCertificateFlags errors,
60 gpointer user_data);
61
62
63// Handle load_changed signal for OAuth completion detection
64static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) {
65 const char* uri = webkit_web_view_get_uri(web_view);
66 unsigned long view_id = (unsigned long)user_data;
67
68 // Only process WEBKIT_LOAD_FINISHED events
69 if (load_event == WEBKIT_LOAD_FINISHED && uri) {
70 printf("[webkit-load] Load finished for URI: %s\n", uri);
71 goHandleLoadChanged(view_id, (char*)uri, load_event);
72 }
73}
74
75// Handle close signal when JavaScript calls window.close()
76static void on_webview_close(WebKitWebView* web_view, gpointer user_data) {
77 unsigned long view_id = (unsigned long)user_data;
78 printf("[webkit-close] WebView close signal from view_id: %lu\n", view_id);
79 goHandleWebViewClose(view_id);
80}
81
82
83// Handle decide-policy signal for new window requests - more robust than create signal
84static gboolean on_decide_policy(WebKitWebView *web_view,
85 WebKitPolicyDecision *decision,
86 WebKitPolicyDecisionType type,
87 gpointer user_data) {
88 // Debug: log all policy decisions
89 const char* type_names[] = {"NAVIGATION_ACTION", "NEW_WINDOW_ACTION", "RESPONSE"};
90 const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN";
91 printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type);
92
93 if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
94 WebKitNavigationPolicyDecision *nav_decision =
95 WEBKIT_NAVIGATION_POLICY_DECISION(decision);
96 WebKitNavigationAction *action =
97 webkit_navigation_policy_decision_get_navigation_action(nav_decision);
98 WebKitURIRequest *request =
99 webkit_navigation_action_get_request(action);
100 const char* uri = webkit_uri_request_get_uri(request);
101
102 // Get navigation type to understand how popup was triggered
103 WebKitNavigationType nav_type = webkit_navigation_action_get_navigation_type(action);
104
105 // Get frame name to distinguish _blank from other popups
106 const char* frame_name = webkit_navigation_action_get_frame_name(action);
107
108 // Get additional properties to better distinguish _blank from popups
109 gboolean is_user_gesture = webkit_navigation_action_is_user_gesture(action);
110 guint modifiers = webkit_navigation_action_get_modifiers(action);
111 guint mouse_button = webkit_navigation_action_get_mouse_button(action);
112
113 // Call Go handler for popup creation
114 unsigned long view_id = (unsigned long)user_data;
115 gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type, (char*)frame_name,
116 is_user_gesture, modifiers, mouse_button);
117
118 if (handled) {
119 webkit_policy_decision_ignore(decision);
120 } else {
121 webkit_policy_decision_use(decision);
122 }
123 return TRUE;
124 }
125 return FALSE;
126}
127
128static void connect_tls_error_handler(WebKitWebView* wv);
129
130
131// Dialog callback structure for modern AlertDialog async handling
132typedef struct {
133 int response;
134 gboolean completed;
135 GMainLoop *loop;
136} AlertDialogResponse;
137
138static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
139static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
140static char* extract_certificate_info(GTlsCertificate* certificate);
141
142// Setup video acceleration environment variables
143static void setup_video_acceleration(const char* driver_name, int enable_all, int legacy) {
144 if (legacy) {
145 // Legacy mode: force old VA-API plugins, disable modern ones
146 setenv("WEBKIT_GST_ENABLE_LEGACY_VAAPI", "1", 1);
147 // Don't set other env vars in legacy mode to avoid conflicts
148 return;
149 }
150
151 // Modern mode (default): use improved VA-API handling
152 if (driver_name && strlen(driver_name) > 0) {
153 if (strcmp(driver_name, "nvidia") == 0) {
154 // NVIDIA uses different env vars for hardware video acceleration
155 setenv("VDPAU_DRIVER", "nvidia", 1);
156 setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
157 } else if (strcmp(driver_name, "vdpau") == 0) {
158 // Explicit VDPAU backend (for NVIDIA via VA-API)
159 setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
160 } else {
161 // AMD/Intel use VA-API directly
162 setenv("LIBVA_DRIVER_NAME", driver_name, 1);
163 }
164 }
165
166 if (enable_all) {
167 setenv("GST_VAAPI_ALL_DRIVERS", "1", 1);
168 }
169}
170
171// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
172static 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) {
173 WebKitUserContentManager* u = webkit_user_content_manager_new();
174 if (!u) return NULL;
175 // WebKitGTK 6: create NetworkSession with data/cache directories
176 WebKitNetworkSession* sess = webkit_network_session_new(
177 data_dir ? data_dir : "",
178 cache_dir ? cache_dir : "");
179 if (!sess) { g_object_unref(u); return NULL; }
180 // Apply memory pressure settings to the session when provided
181 if (pressure_settings) {
182 // WebKitGTK 6.0 API: setter expects only the settings pointer
183 webkit_network_session_set_memory_pressure_settings(pressure_settings);
184 }
185 // Configure cookie persistence and policy (no third-party by default)
186 WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
187 if (cm) {
188 if (cookie_path && cookie_path[0]) {
189 webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
190 }
191 // 2 => no-third-party (default), 0 => always, 1 => never
192 maybe_set_cookie_policy(cm, 2);
193 }
194 // Persist credentials (HTTP auth, etc.)
195 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
196 // Configure TLS errors to emit signals instead of failing silently
197 webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
198 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
199 "user-content-manager", u,
200 "network-session", sess,
201 NULL));
202 // WebView holds refs to provided objects; drop our temporary refs
203 g_object_unref(sess);
204 // Set WebView background to black (easier on eyes when pages are loading)
205 GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
206 webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
207
208 // Connect TLS error handler
209 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
210
211 if (out_ucm) { *out_ucm = u; }
212 return w;
213}
214
215// Create a new WebView related to an existing one for process sharing
216static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserContentManager** out_ucm) {
217 if (!related_view) {
218 // Fallback to regular WebView creation - we need to provide the required parameters
219 return new_webview_with_ucm_and_session(NULL, NULL, NULL, out_ucm, NULL);
220 }
221
222 // Get the network session and user content manager from the related view
223 WebKitNetworkSession* related_session = webkit_web_view_get_network_session(related_view);
224 WebKitUserContentManager* related_ucm = webkit_web_view_get_user_content_manager(related_view);
225
226 // Create content manager if needed
227 WebKitUserContentManager* u = related_ucm;
228 if (!u) {
229 u = webkit_user_content_manager_new();
230 } else {
231 g_object_ref(u); // Add ref since we'll pass it to new WebView
232 }
233
234 // Create WebView with related view for process sharing (network session is inherited)
235 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
236 "user-content-manager", u,
237 "related-view", related_view,
238 NULL));
239
240 // Connect TLS error handler
241 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
242
243 // WebView holds refs to provided objects; drop our temporary refs
244 g_object_unref(u);
245 if (out_ucm) { *out_ucm = u; }
246 return w;
247}
248
249static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) {
250 if (!web_view) return;
251 // Create signal is now handled in window_handler_cgo.go
252 // Use ConnectCreateSignal from Go to connect the signal
253 printf("[webkit-debug] Policy handler connected (create signal handled separately)\n");
254}
255
256// Connect close signal to a WebView for popup auto-close functionality
257static void connect_close_signal(WebKitWebView* web_view, unsigned long id) {
258 if (!web_view) return;
259 g_signal_connect_data(G_OBJECT(web_view), "close", G_CALLBACK(on_webview_close), (gpointer)id, NULL, 0);
260 printf("[webkit-connect] Connected close signal to WebView with id: %lu\n", id);
261}
262
263// Callback for print signal - intercepts Ctrl+P and can redirect behavior
264static gboolean on_print_request(WebKitWebView* web_view, WebKitPrintOperation* print_operation, gpointer user_data) {
265 (void)web_view;
266 (void)print_operation;
267 (void)user_data;
268
269 // Return TRUE to prevent default print dialog
270 // Return FALSE to allow default print behavior
271 // For now, we'll block the print dialog entirely since Ctrl+P should trigger pane mode
272 printf("[webkit-print] Print request intercepted - blocking to prevent conflict with pane mode\n");
273 return TRUE; // Block the print dialog
274}
275
276// Connect print signal to a WebView to intercept Ctrl+P
277static void connect_print_signal(WebKitWebView* web_view, unsigned long id) {
278 if (!web_view) return;
279 g_signal_connect_data(G_OBJECT(web_view), "print", G_CALLBACK(on_print_request), (gpointer)id, NULL, 0);
280 printf("[webkit-connect] Connected print signal to WebView with id: %lu\n", id);
281}
282
283static gboolean gtk_prefers_dark() {
284 // Method 1: Check GNOME desktop interface color-scheme (primary method)
285 GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
286 if (desktop_settings) {
287 gchar* color_scheme = g_settings_get_string(desktop_settings, "color-scheme");
288 if (color_scheme) {
289 gboolean prefer_dark = (g_strcmp0(color_scheme, "prefer-dark") == 0);
290 g_free(color_scheme);
291 g_object_unref(desktop_settings);
292 if (prefer_dark) return TRUE;
293 }
294 g_object_unref(desktop_settings);
295 }
296
297 // Method 2: Check GTK theme name for dark variants
298 GtkSettings* settings = gtk_settings_get_default();
299 if (settings) {
300 gchar* theme_name = NULL;
301 g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
302 if (theme_name) {
303 gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
304 strstr(theme_name, "-Dark") != NULL);
305 g_free(theme_name);
306 if (is_dark) return TRUE;
307 }
308
309 // Method 3: Check gtk-application-prefer-dark-theme (fallback)
310 gboolean prefer = FALSE;
311 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
312 if (prefer) return TRUE;
313 }
314
315 return FALSE;
316}
317
318// Note: preferred color scheme is handled via a user script injection to support
319// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
320
321extern void goOnUcmMessage(unsigned long id, char* json);
322
323// Helper functions to connect signals using static callbacks
324static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
325 if (!widget) return;
326 g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
327}
328
329static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
330 if (!widget) return;
331 g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
332}
333
334static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
335 if (!widget) return;
336 g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
337}
338
339static void connect_favicon_changed_with_id(GtkWidget* widget, unsigned long id) {
340 if (!widget) return;
341 WebKitWebView* web_view = WEBKIT_WEB_VIEW(widget);
342 WebKitNetworkSession* network_session = webkit_web_view_get_network_session(web_view);
343 if (network_session) {
344 WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(network_session);
345 if (data_manager) {
346 WebKitFaviconDatabase* favicon_db = webkit_website_data_manager_get_favicon_database(data_manager);
347 if (favicon_db) {
348 g_signal_connect_data(G_OBJECT(favicon_db), "favicon-changed", G_CALLBACK(on_favicon_changed), (gpointer)id, NULL, 0);
349 printf("[favicon-c] Connected favicon-changed signal for WebView ID %lu\n", id);
350 } else {
351 printf("[favicon-c] Failed to get favicon database\n");
352 }
353 } else {
354 printf("[favicon-c] Failed to get website data manager for favicon signal\n");
355 }
356 } else {
357 printf("[favicon-c] Failed to get network session\n");
358 }
359}
360
361static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
362 if (!settings) return;
363 g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
364}
365
366static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
367 if (!context || !scheme) return;
368 webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
369}
370static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) {
371 if (!context || !scheme) return;
372 WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context);
373 if (!sm) return;
374
375 // Mark as secure to avoid mixed-content warnings when requested by https pages
376 webkit_security_manager_register_uri_scheme_as_secure(sm, scheme);
377 // Treat as local scheme
378 webkit_security_manager_register_uri_scheme_as_local(sm, scheme);
379 // Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme)
380 webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
381}
382
383static void enable_favicon_database(WebKitNetworkSession* session) {
384 if (!session) return;
385 WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(session);
386 if (data_manager) {
387 webkit_website_data_manager_set_favicons_enabled(data_manager, TRUE);
388 printf("[favicon-c] Favicon database enabled\n");
389 } else {
390 printf("[favicon-c] Failed to get website data manager\n");
391 }
392}
393
394// Script message handler wiring for WebKitGTK 6
395// Forward declaration for callback used in g_signal_connect_data
396// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
397static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
398// Uses WebKitScriptMessage and script-message-received::NAME signal.
399static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
400 (void)ucm;
401 if (!val) return;
402 gchar* s = jsc_value_to_string(val);
403 if (!s) return;
404 goOnUcmMessage((unsigned long)user_data, s);
405 g_free(s);
406}
407
408static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
409 if (!ucm || !name) return FALSE;
410 // WebKitGTK 6 signature: (ucm, name, world_name)
411 webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
412 gchar* signal = g_strdup_printf("script-message-received::%s", name);
413 if (!signal) return FALSE;
414 g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
415 g_free(signal);
416 return TRUE;
417}
418
419// Register a script message handler for a specific JavaScript world
420static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) {
421 if (!ucm || !name || !world_name) return FALSE;
422 webkit_user_content_manager_register_script_message_handler(ucm, name, world_name);
423 gchar* signal = g_strdup_printf("script-message-received::%s", name);
424 if (!signal) return FALSE;
425 g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
426 g_free(signal);
427 return TRUE;
428}
429
430// ----- Conditional helpers for settings across WebKit versions -----
431#ifndef WEBKIT_CHECK_VERSION
432#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
433#endif
434
435static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
436#if WEBKIT_CHECK_VERSION(2,40,0)
437 // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
438 if (!settings) return;
439 if (mode == 1) {
440 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
441 } else if (mode == 2) {
442 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
443 } else {
444 // Leave default policy (auto) without forcing a value to avoid referencing
445 // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
446 }
447#else
448 (void)settings; (void)mode;
449#endif
450}
451
452static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
453#if WEBKIT_CHECK_VERSION(2,36,0)
454 if (!settings) return;
455 webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
456#else
457 (void)settings; (void)enable;
458#endif
459}
460
461static void maybe_set_webgl(WebKitSettings* settings, int enable) {
462#if WEBKIT_CHECK_VERSION(2,20,0)
463 if (!settings) return;
464 webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
465#else
466 (void)settings; (void)enable;
467#endif
468}
469
470static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
471#if WEBKIT_CHECK_VERSION(2,20,0)
472 if (!settings) return;
473 webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
474#else
475 (void)settings; (void)enable;
476#endif
477}
478
479static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
480#if WEBKIT_CHECK_VERSION(2,20,0)
481 if (!settings) return;
482 webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
483#else
484 (void)settings; (void)require;
485#endif
486}
487
488static void maybe_set_smooth_scrolling(WebKitSettings* settings, int enable) {
489#if WEBKIT_CHECK_VERSION(2,10,0)
490 if (!settings) return;
491 webkit_settings_set_enable_smooth_scrolling(settings, enable ? TRUE : FALSE);
492#else
493 (void)settings; (void)enable;
494#endif
495}
496
497// Enable two-finger swipe back/forward navigation gestures when supported
498static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
499#if WEBKIT_CHECK_VERSION(2,38,0)
500 if (!settings) return;
501 webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
502#else
503 (void)settings; (void)enable;
504#endif
505}
506
507// Set cookie accept policy with compile-time compatibility across WebKit versions
508static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
509 if (!cm) return;
510#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
511 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
512 if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
513 else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
514 webkit_cookie_manager_set_accept_policy(cm, p);
515#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
516 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
517 if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
518 else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
519 webkit_cookie_manager_set_accept_policy(cm, p);
520#else
521 (void)policy; // Not supported in this header version
522#endif
523}
524
525// Memory pressure settings helpers (version-guarded)
526static WebKitMemoryPressureSettings* create_memory_pressure_settings(
527 unsigned int memory_limit_mb,
528 double conservative_threshold,
529 double strict_threshold,
530 double kill_threshold,
531 double poll_interval) {
532 // If headers don't expose memory pressure APIs, return NULL to signal unsupported
533#if WEBKIT_CHECK_VERSION(2,44,0)
534 WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
535 if (!settings) return NULL;
536
537 if (memory_limit_mb > 0) {
538 webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
539 }
540
541 webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
542 webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
543
544 if (kill_threshold > 0) {
545 webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
546 }
547
548 webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
549
550 return settings;
551#else
552 (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
553 return NULL;
554#endif
555}
556
557// Cache model configuration
558static void maybe_set_cache_model(WebKitWebContext* context, int model) {
559 if (!context) return;
560 WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
561
562 switch (model) {
563 case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
564 case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
565 case 2:
566#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
567 cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
568#else
569 cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
570#endif
571 break;
572 }
573
574 webkit_web_context_set_cache_model(context, cache_model);
575}
576
577// Additional settings for memory optimization
578static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
579#if WEBKIT_CHECK_VERSION(2,6,0)
580 if (!settings) return;
581 webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
582#else
583 (void)settings; (void)enable;
584#endif
585}
586
587// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
588static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
589 (void)settings; (void)enable;
590}
591
592// Apply memory pressure settings to context if available
593static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
594 // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
595 (void)context; (void)settings;
596}
597
598// Trigger JavaScript GC if supported by headers
599static void maybe_collect_js(WebKitWebContext* context) {
600 // Disabled by default due to API availability variance across 6.0 builds.
601 (void)context;
602}
603
604// TLS error handling implementation
605static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
606 const char *failing_uri,
607 GTlsCertificate *certificate,
608 GTlsCertificateFlags errors,
609 gpointer user_data) {
610 (void)user_data;
611
612 // Extract host from URI for the Go callback
613 const char* host = failing_uri;
614 char* host_copy = NULL;
615
616 if (strstr(failing_uri, "://")) {
617 host = strstr(failing_uri, "://") + 3;
618 char* slash = strchr(host, '/');
619 if (slash) {
620 // Create a temporary null-terminated host string
621 size_t host_len = slash - host;
622 host_copy = malloc(host_len + 1);
623 if (host_copy) {
624 strncpy(host_copy, host, host_len);
625 host_copy[host_len] = '\0';
626 host = host_copy;
627 }
628 }
629 }
630
631 // Extract certificate information
632 char* cert_info = extract_certificate_info(certificate);
633
634 gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
635
636 // If user accepted, allow the certificate for this host
637 if (should_proceed && certificate) {
638 printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
639 fflush(stdout);
640 // Get the network session from the web view
641 WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
642 if (session) {
643 webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
644 printf("[dumber] Certificate exception added for host: %s\n", host);
645 fflush(stdout);
646
647 // Try loading the URL again after adding the exception
648 webkit_web_view_load_uri(web_view, failing_uri);
649 printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
650 fflush(stdout);
651 }
652 }
653
654 // Free the certificate info string
655 if (cert_info) {
656 g_free(cert_info);
657 }
658
659 if (host_copy) {
660 free(host_copy);
661 }
662
663 return should_proceed;
664}
665
666// Extract certificate information for display
667static char* extract_certificate_info(GTlsCertificate* certificate) {
668 if (!certificate) {
669 return g_strdup("Certificate information not available");
670 }
671
672 printf("[dumber] Extracting certificate information...\n");
673 fflush(stdout);
674
675 GString* info = g_string_new("Certificate Information:\n");
676
677 // Try to get certificate properties - but these might not be available in all GTK versions
678 gchar* subject = NULL;
679 gchar* issuer = NULL;
680 GDateTime* not_valid_before = NULL;
681 GDateTime* not_valid_after = NULL;
682
683 // Try property access but handle failures gracefully
684 g_object_get(certificate,
685 "subject-name", &subject,
686 "issuer-name", &issuer,
687 "not-valid-before", ¬_valid_before,
688 "not-valid-after", ¬_valid_after,
689 NULL);
690
691 if (subject) {
692 g_string_append_printf(info, "Subject: %s\n", subject);
693 g_free(subject);
694 printf("[dumber] Found subject info\n");
695 } else {
696 g_string_append(info, "Subject: Information not available\n");
697 }
698
699 if (issuer) {
700 g_string_append_printf(info, "Issued by: %s\n", issuer);
701 g_free(issuer);
702 printf("[dumber] Found issuer info\n");
703 } else {
704 g_string_append(info, "Issued by: Information not available\n");
705 }
706
707 if (not_valid_before) {
708 gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
709 g_string_append_printf(info, "Valid from: %s\n", date_str);
710 g_free(date_str);
711 g_date_time_unref(not_valid_before);
712 } else {
713 g_string_append(info, "Valid from: Information not available\n");
714 }
715
716 if (not_valid_after) {
717 gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
718 g_string_append_printf(info, "Valid until: %s\n", date_str);
719 g_free(date_str);
720 g_date_time_unref(not_valid_after);
721 } else {
722 g_string_append(info, "Valid until: Information not available\n");
723 }
724
725 char* result = g_string_free(info, FALSE);
726 printf("[dumber] Certificate info: %s\n", result);
727 fflush(stdout);
728 return result;
729}
730
731static void connect_tls_error_handler(WebKitWebView* wv) {
732 if (!wv) return;
733 g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
734 G_CALLBACK(on_load_failed_with_tls_errors), NULL);
735}
736
737// Modern AlertDialog async callback
738static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
739 AlertDialogResponse *data = (AlertDialogResponse*)user_data;
740 GError *error = NULL;
741 data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
742 if (error) {
743 printf("[dumber] AlertDialog error: %s\n", error->message);
744 g_error_free(error);
745 data->response = 0;
746 }
747 data->completed = TRUE;
748 const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
749 const char *response_name = (data->response >= 0 && data->response <= 2) ?
750 response_names[data->response] : "UNKNOWN";
751 printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
752 fflush(stdout);
753 if (data->loop) {
754 g_main_loop_quit(data->loop);
755 }
756}
757
758// Show TLS warning dialog synchronously using modern GtkAlertDialog API
759static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
760 printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
761 printf("[dumber] Error message: %s\n", error_msg);
762 fflush(stdout);
763
764 // Create the modern AlertDialog
765 GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
766
767 // Set modal behavior and error details
768 gtk_alert_dialog_set_modal(dialog, TRUE);
769 gtk_alert_dialog_set_detail(dialog, error_msg);
770
771 // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
772 const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
773 gtk_alert_dialog_set_buttons(dialog, buttons);
774 gtk_alert_dialog_set_default_button(dialog, 0);
775 gtk_alert_dialog_set_cancel_button(dialog, 0);
776
777 // Set up response data with main loop for sync behavior
778 AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
779
780 // Start async operation
781 gtk_alert_dialog_choose(dialog, parent, NULL,
782 on_alert_dialog_choose_done, &response_data);
783
784 // Run event loop until dialog completes
785 g_main_loop_run(response_data.loop);
786 g_main_loop_unref(response_data.loop);
787
788 // Clean up
789 g_object_unref(dialog);
790
791 // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
792 return response_data.response;
793}
794
795*/
796import "C"
797
798import (
799 "context"
800 "crypto/sha256"
801 "database/sql"
802 "encoding/json"
803 "errors"
804 "fmt"
805 "log"
806 "math"
807 "os"
808 "path/filepath"
809 "runtime"
810 "runtime/cgo"
811 "strconv"
812 "strings"
813 "sync"
814 "time"
815 "unsafe"
816
817 "github.com/bnema/dumber/internal/config"
818 "github.com/bnema/dumber/internal/db"
819 "github.com/bnema/dumber/internal/logging"
820 _ "github.com/ncruces/go-sqlite3"
821)
822
823// FilterManager interface for content blocking integration
824type FilterManager interface {
825 GetNetworkFilters() ([]byte, error)
826 GetCosmeticScript() string
827 GetCosmeticScriptForDomain(domain string) string
828}
829
830// domBridgeTemplate removed - now loading compiled main-world.min.js
831
832var registerSchemeOnce sync.Once
833
834func registerDumbScheme(ctx *C.WebKitWebContext) {
835 if ctx == nil {
836 return
837 }
838
839 registerSchemeOnce.Do(func() {
840 sch := C.CString("dumb")
841 defer C.free(unsafe.Pointer(sch))
842 C.register_uri_scheme_handler(ctx, sch)
843 C.register_uri_scheme_security(ctx, sch)
844 })
845}
846
847// Helper function to convert bool to int for C interop
848func boolToInt(b bool) int {
849 if b {
850 return 1
851 }
852 return 0
853}
854
855//export goInvokeHandle
856func goInvokeHandle(handle C.uintptr_t) {
857 h := cgo.Handle(handle)
858 if fn, ok := h.Value().(func()); ok {
859 fn()
860 }
861 h.Delete()
862}
863
864// getCertificateHash generates a SHA256 hash of the certificate info for storage
865func getCertificateHash(certificateInfo string) string {
866 hash := sha256.Sum256([]byte(certificateInfo))
867 return fmt.Sprintf("%x", hash)
868}
869
870// getDBConnection opens a database connection and returns queries
871func getDBConnection() (*db.Queries, *sql.DB, error) {
872 cfg := config.Get()
873 if cfg == nil {
874 return nil, nil, fmt.Errorf("config not available")
875 }
876
877 dbPath := cfg.Database.Path
878 if dbPath == "" {
879 var err error
880 dbPath, err = config.GetDatabaseFile()
881 if err != nil {
882 return nil, nil, fmt.Errorf("failed to get database path: %w", err)
883 }
884 }
885
886 database, err := sql.Open("sqlite3", dbPath)
887 if err != nil {
888 return nil, nil, fmt.Errorf("failed to open database: %w", err)
889 }
890
891 queries := db.New(database)
892 return queries, database, nil
893}
894
895// checkStoredCertificateDecision checks if user has previously decided on this certificate
896func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
897 queries, database, err := getDBConnection()
898 if err != nil {
899 log.Printf("[tls] Failed to connect to database: %v", err)
900 return "", false
901 }
902 defer database.Close()
903
904 certHash := getCertificateHash(certificateInfo)
905 ctx := context.Background()
906
907 validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
908 if err != nil {
909 if err == sql.ErrNoRows {
910 return "", false // No previous decision
911 }
912 log.Printf("[tls] Failed to query certificate validation: %v", err)
913 return "", false
914 }
915
916 log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
917 return validation.UserDecision, true
918}
919
920// storeCertificateDecision stores the user's decision about a certificate
921func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
922 queries, database, err := getDBConnection()
923 if err != nil {
924 return fmt.Errorf("failed to connect to database: %w", err)
925 }
926 defer database.Close()
927
928 certHash := getCertificateHash(certificateInfo)
929 ctx := context.Background()
930
931 var expiresAt sql.NullTime
932 if !permanent {
933 // For temporary decisions, expire after 24 hours
934 expiresAt = sql.NullTime{
935 Time: time.Now().Add(24 * time.Hour),
936 Valid: true,
937 }
938 }
939
940 err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
941 if err != nil {
942 return fmt.Errorf("failed to store certificate validation: %w", err)
943 }
944
945 log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
946 return nil
947}
948
949type nativeView struct {
950 win *C.GtkWidget
951 container *C.GtkWidget
952 view *C.GtkWidget
953 wv *C.WebKitWebView
954 ucm *C.WebKitUserContentManager
955}
956
957type memoryStats struct {
958 pageLoadCount int
959 lastGCTime time.Time
960 memoryPressureSettings *C.WebKitMemoryPressureSettings
961}
962
963// WebView represents a browser view powered by WebKit2GTK.
964type WebView struct {
965 config *Config
966 zoom float64
967 url string
968 destroyed bool
969 native *nativeView
970 window *Window
971 id uintptr
972 msgHandler func(payload string)
973 titleHandler func(title string)
974 uriHandler func(uri string)
975 faviconHandler func(data []byte)
976 faviconURIHandler func(pageURI, faviconURI string)
977 zoomHandler func(level float64)
978 popupHandler func(string) *WebView
979 closeHandler func()
980 memStats *memoryStats
981 gcTicker *time.Ticker
982 gcDone chan struct{}
983 tlsExceptions map[string]bool // host -> whether user allowed certificate exception
984 useDomZoom bool
985 domZoomSeed float64
986 domBridgeScript *C.WebKitUserScript
987 wasReparented bool // Track if this WebView has been reparented
988
989 // Window type detection and relationships
990 windowType WindowType
991 windowFeatures *WindowFeatures
992 windowTypeCallback func(WindowType, *WindowFeatures)
993 parentWebView *WebView
994 isRelated bool
995 isActive bool // Track if this WebView is currently active/focused
996}
997
998// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
999func NewWebView(cfg *Config) (*WebView, error) {
1000 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
1001 if C.gtk_init_check() == 0 {
1002 return nil, errors.New("failed to initialize GTK")
1003 }
1004
1005 if cfg == nil {
1006 cfg = &Config{}
1007 }
1008
1009 // Setup video acceleration environment variables before creating WebView
1010 if cfg.VideoAcceleration.EnableVAAPI {
1011 driverName := C.CString(cfg.VideoAcceleration.VAAPIDriverName)
1012 enableAll := 0
1013 if cfg.VideoAcceleration.EnableAllDrivers {
1014 enableAll = 1
1015 }
1016 legacy := 0
1017 if cfg.VideoAcceleration.LegacyVAAPI {
1018 legacy = 1
1019 }
1020 C.setup_video_acceleration(driverName, C.int(enableAll), C.int(legacy))
1021 C.free(unsafe.Pointer(driverName))
1022 }
1023
1024 // Prepare persistent website data/caches
1025 dataDir := cfg.DataDir
1026 cacheDir := cfg.CacheDir
1027 if dataDir == "" {
1028 dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
1029 }
1030 if cacheDir == "" {
1031 cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
1032 }
1033 _ = os.MkdirAll(dataDir, 0o755)
1034 _ = os.MkdirAll(cacheDir, 0o755)
1035
1036 cData := C.CString(dataDir)
1037 cCache := C.CString(cacheDir)
1038 cookieFile := filepath.Join(dataDir, "cookies.db")
1039 cCookie := C.CString(cookieFile)
1040 defer C.free(unsafe.Pointer(cData))
1041 defer C.free(unsafe.Pointer(cCache))
1042 defer C.free(unsafe.Pointer(cCookie))
1043 // Use default WebContext to register the custom URI scheme handler; the
1044 // WebView itself is created with a WebKitNetworkSession for persistence.
1045 ctx := C.webkit_web_context_get_default()
1046
1047 // Enable favicon database - we'll do this per WebView when created since we need NetworkSession
1048
1049 // Apply cache model to WebContext
1050 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
1051 cacheModel := 1
1052 if cfg.Memory.CacheModel != "" {
1053 switch cfg.Memory.CacheModel {
1054 case CacheModelWebBrowser:
1055 cacheModel = 1
1056 case CacheModelPrimaryWebBrowser:
1057 cacheModel = 2
1058 default: // CacheModelDocumentViewer or unset
1059 cacheModel = 0
1060 }
1061 }
1062 C.maybe_set_cache_model(ctx, C.int(cacheModel))
1063
1064 // Configure memory pressure settings if specified
1065 var pressureSettings *C.WebKitMemoryPressureSettings
1066 // Only configure memory pressure if explicitly requested via config
1067 if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
1068 memLimitMB := cfg.Memory.MemoryLimitMB
1069 conservativeThreshold := cfg.Memory.ConservativeThreshold
1070 strictThreshold := cfg.Memory.StrictThreshold
1071 killThreshold := cfg.Memory.KillThreshold
1072 pollInterval := cfg.Memory.PollIntervalSeconds
1073
1074 pressureSettings = C.create_memory_pressure_settings(
1075 C.uint(memLimitMB),
1076 C.double(conservativeThreshold),
1077 C.double(strictThreshold),
1078 C.double(killThreshold),
1079 C.double(pollInterval))
1080
1081 if pressureSettings != nil {
1082 C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
1083 if cfg.Memory.EnableMemoryMonitoring {
1084 log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
1085 memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
1086 }
1087 }
1088 }
1089
1090 registerDumbScheme(ctx)
1091 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
1092
1093 // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
1094 var createdUcm *C.WebKitUserContentManager
1095 viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
1096 if viewWidget == nil {
1097 return nil, errors.New("failed to create WebKitWebView")
1098 }
1099
1100 var win *C.GtkWidget
1101 var window *Window
1102 container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0)
1103 if container == nil {
1104 return nil, errors.New("failed to create GtkBox container")
1105 }
1106 // Hold a strong reference so the WebView keeps the container alive across reparenting.
1107 C.g_object_ref_sink(C.gpointer(container))
1108 C.gtk_widget_set_hexpand(container, C.gboolean(1))
1109 C.gtk_widget_set_vexpand(container, C.gboolean(1))
1110 C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1))
1111 C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1))
1112 C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget)
1113
1114 // Only create window if requested
1115 if cfg.CreateWindow {
1116 // Create a top-level window to host the view
1117 win = C.new_window()
1118 if win == nil {
1119 return nil, errors.New("failed to create GtkWindow")
1120 }
1121
1122 // Pack view widget into the window
1123 // GTK4: containers removed; use gtk_window_set_child
1124 C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container)
1125 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
1126 C.connect_destroy_quit(win)
1127 window = &Window{win: win}
1128 }
1129
1130 seed := 1.0
1131 if cfg.ZoomDefault > 0 {
1132 seed = cfg.ZoomDefault
1133 }
1134
1135 v := &WebView{
1136 config: cfg,
1137 zoom: 1.0,
1138 useDomZoom: cfg.UseDomZoom,
1139 domZoomSeed: seed,
1140 native: &nativeView{win: win, container: container, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
1141 window: window,
1142 memStats: &memoryStats{
1143 pageLoadCount: 0,
1144 lastGCTime: time.Now(),
1145 memoryPressureSettings: pressureSettings,
1146 },
1147 gcDone: make(chan struct{}),
1148 tlsExceptions: make(map[string]bool),
1149 }
1150 // Assign an ID for accelerator dispatch
1151 v.id = nextViewID()
1152 registerView(v.id, v)
1153 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1154 ConnectCreateSignal(v) // Connect create signal for window.open support
1155
1156 // Register with global memory manager if monitoring is enabled
1157 if cfg.Memory.EnableMemoryMonitoring {
1158 if globalMemoryManager == nil {
1159 InitializeGlobalMemoryManager(
1160 cfg.Memory.EnableMemoryMonitoring,
1161 cfg.Memory.ProcessRecycleThreshold,
1162 30*time.Second, // Default monitoring interval
1163 )
1164 }
1165 if globalMemoryManager != nil {
1166 globalMemoryManager.RegisterWebView(v)
1167 }
1168 }
1169 // Attach GTK4 input controllers (keyboard, mouse)
1170 AttachKeyboardControllers(v)
1171
1172 // Enable print interception to prevent Ctrl+P conflicts with pane mode
1173 v.EnablePrintInterception()
1174
1175 // Watch for GTK theme changes and propagate to page at runtime
1176 if settings := C.gtk_settings_get_default(); settings != nil {
1177 C.connect_theme_changed_with_id(settings, C.ulong(v.id))
1178 }
1179
1180 // Zoom shortcuts are now handled by application-level workspace shortcuts
1181 // in workspace_manager.go to ensure proper active pane targeting
1182 // Find in page (Ctrl/Cmd+F): handled by window-level shortcuts in window_shortcuts.go
1183 // Navigation with Ctrl/Cmd + Arrow keys: forward to keyboard service bridge
1184 _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowLeft", func() {
1185 _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowleft'}}))")
1186 })
1187 _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowRight", func() {
1188 _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowright'}}))")
1189 })
1190 // Update window title when page title changes
1191 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
1192 // Also dispatch title change to Go with view id
1193 C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
1194 // Notify URI changes to Go to keep current URL in sync
1195 C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
1196 // Enable favicon database for this WebView's NetworkSession
1197 networkSession := C.webkit_web_view_get_network_session(v.native.wv)
1198 C.enable_favicon_database(networkSession)
1199
1200 // Notify favicon changes to Go for caching
1201 log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id)
1202 C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id))
1203 // Apply hardware acceleration and related settings based on cfg.Rendering
1204 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1205 // Hardware acceleration policy (guarded by version)
1206 switch cfg.Rendering.Mode {
1207 case "gpu":
1208 log.Printf("[webkit] Applying GPU rendering mode")
1209 C.maybe_set_hw_policy(settings, 1)
1210 C.maybe_set_webgl(settings, 1)
1211 C.maybe_set_canvas_accel(settings, 1)
1212 case "cpu":
1213 log.Printf("[webkit] Applying CPU rendering mode")
1214 C.maybe_set_hw_policy(settings, 2)
1215 C.maybe_set_webgl(settings, 0)
1216 C.maybe_set_canvas_accel(settings, 0)
1217 default: // auto
1218 log.Printf("[webkit] Applying AUTO rendering mode - detecting actual backend...")
1219 C.maybe_set_hw_policy(settings, 0)
1220 C.maybe_set_webgl(settings, 1)
1221 C.maybe_set_canvas_accel(settings, 1)
1222 // Detect actual rendering backend after applying auto settings
1223 v.detectAndLogRenderingBackend()
1224 }
1225 // Optional compositing indicators for debugging (guarded by version)
1226 if cfg.Rendering.DebugGPU {
1227 C.maybe_set_draw_indicators(settings, 1)
1228 }
1229 // Reduce media pipeline churn by requiring a user gesture for playback
1230 C.maybe_set_media_user_gesture(settings, 1)
1231 // Enable trackpad back/forward gestures when available
1232 C.maybe_set_back_forward_gestures(settings, 1)
1233 // Smooth scrolling for wheel/pinch gestures when supported
1234 C.maybe_set_smooth_scrolling(settings, 1)
1235
1236 // Keep page cache enabled by default for performance
1237 enablePageCache := true
1238 if cfg.Memory.EnablePageCache {
1239 enablePageCache = true
1240 }
1241 C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
1242
1243 // Offline app cache is deprecated; keep as no-op
1244 C.maybe_set_offline_app_cache(settings, C.int(0))
1245
1246 if cfg.Memory.EnableMemoryMonitoring {
1247 log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
1248 }
1249 }
1250
1251 if cfg.ZoomDefault > 0 {
1252 v.zoom = cfg.ZoomDefault
1253 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
1254 }
1255 if cfg.EnableDeveloperExtras {
1256 settings := C.webkit_web_view_get_settings(v.native.wv)
1257 if settings != nil {
1258 C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
1259 }
1260 }
1261
1262 // Enable JavaScript popup windows for workspace popup handling
1263 settings := C.webkit_web_view_get_settings(v.native.wv)
1264 if settings != nil {
1265 C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1))
1266 log.Printf("[webkit] Enabled JavaScript popup windows")
1267 }
1268
1269 // Apply custom User-Agent for codec negotiation
1270 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1271 if cfg.CodecPreferences.CustomUserAgent != "" {
1272 cUserAgent := C.CString(cfg.CodecPreferences.CustomUserAgent)
1273 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cUserAgent))
1274 C.free(unsafe.Pointer(cUserAgent))
1275 log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
1276 } else if cfg.CodecPreferences.ForceAV1 {
1277 // Use modern Chrome UA that signals AV1 support
1278 av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1279 cAV1UA := C.CString(av1UA)
1280 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
1281 C.free(unsafe.Pointer(cAV1UA))
1282 log.Printf("[webkit] Set AV1-optimized User-Agent")
1283 }
1284 }
1285
1286 // Apply default fonts if provided in cfg
1287 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1288 if cfg.DefaultSansFont != "" {
1289 csans := C.CString(cfg.DefaultSansFont)
1290 C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
1291 C.free(unsafe.Pointer(csans))
1292 // Also set default font family to sans
1293 csans2 := C.CString(cfg.DefaultSansFont)
1294 C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
1295 C.free(unsafe.Pointer(csans2))
1296 }
1297 if cfg.DefaultSerifFont != "" {
1298 cserif := C.CString(cfg.DefaultSerifFont)
1299 C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
1300 C.free(unsafe.Pointer(cserif))
1301 }
1302 if cfg.DefaultMonospaceFont != "" {
1303 cmono := C.CString(cfg.DefaultMonospaceFont)
1304 C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
1305 C.free(unsafe.Pointer(cmono))
1306 }
1307 if cfg.DefaultFontSize > 0 {
1308 C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
1309 }
1310 }
1311 if cfg.InitialURL != "" {
1312 _ = v.LoadURL(cfg.InitialURL)
1313 }
1314 // Ensure native resources are released if user forgets to call Destroy
1315 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
1316 log.Printf("[webkit] WebView created (CGO)")
1317
1318 // Initialize UCM scripts and handlers
1319 if v.native != nil && v.native.ucm != nil {
1320 v.enableUserContentManager(cfg)
1321 }
1322
1323 // Start periodic JavaScript garbage collection if configured
1324 if cfg.Memory.EnableGCInterval > 0 {
1325 v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
1326 if cfg.Memory.EnableMemoryMonitoring {
1327 log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
1328 }
1329 }
1330
1331 // Setup console message logging
1332 v.setupConsoleLogging()
1333
1334 return v, nil
1335}
1336
1337// setupConsoleLogging enables console message capture for the WebView
1338func (v *WebView) setupConsoleLogging() {
1339 if v.native == nil || v.native.wv == nil {
1340 return
1341 }
1342
1343 // Check if console capture is enabled in config
1344 cfg := config.Get()
1345 if !cfg.Logging.CaptureConsole {
1346 // Disable console messages to stdout (always, to keep backend clean)
1347 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1348 C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1349 }
1350 return
1351 }
1352
1353 // If console capture is enabled, disable stdout and enable our capture
1354 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1355 C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1356 log.Printf("[webkit] Console message capture enabled - redirecting to console.log file")
1357 }
1358
1359 // TODO: Implement JavaScript injection for console capture
1360 // This will inject code to intercept console.log/error/warn and send to Go
1361}
1362
1363// setupGlobalConsoleCapture sets up a global stdout filter to capture console messages
1364func setupGlobalConsoleCapture() {
1365 // This should be called once globally, not per WebView
1366 logging.StartConsoleCapture()
1367}
1368
1369// injectConsoleCapture injects JavaScript to capture console messages
1370func (v *WebView) injectConsoleCapture() {
1371 consoleScript := `
1372(function() {
1373 const originalLog = console.log;
1374 const originalWarn = console.warn;
1375 const originalError = console.error;
1376 const originalInfo = console.info;
1377 const originalDebug = console.debug;
1378
1379 function sendToGo(level, args) {
1380 const message = args.map(arg =>
1381 typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1382 ).join(' ');
1383
1384 const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message;
1385
1386 // Send to Go via user script message
1387 if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) {
1388 window.webkit.messageHandlers.dumberBridge.postMessage({
1389 type: 'console',
1390 data: formattedMessage
1391 });
1392 }
1393 }
1394
1395 console.log = function(...args) {
1396 originalLog.apply(console, args);
1397 sendToGo('LOG', args);
1398 };
1399
1400 console.warn = function(...args) {
1401 originalWarn.apply(console, args);
1402 sendToGo('WARN', args);
1403 };
1404
1405 console.error = function(...args) {
1406 originalError.apply(console, args);
1407 sendToGo('ERROR', args);
1408 };
1409
1410 console.info = function(...args) {
1411 originalInfo.apply(console, args);
1412 sendToGo('INFO', args);
1413 };
1414
1415 console.debug = function(...args) {
1416 originalDebug.apply(console, args);
1417 sendToGo('DEBUG', args);
1418 };
1419})();
1420`
1421 _ = v.InjectScript(consoleScript)
1422}
1423
1424// handleConsoleMessage processes console messages from JavaScript
1425func (v *WebView) handleConsoleMessage(payload string) {
1426 // Parse the message payload to check if it's a console message
1427 var msg struct {
1428 Type string `json:"type"`
1429 Data string `json:"data"`
1430 }
1431
1432 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
1433 return
1434 }
1435
1436 if msg.Type == "console" {
1437 // Send to logging system with [CONSOLE] tag
1438 logging.CaptureWebKitLog(msg.Data)
1439 }
1440}
1441
1442// startConsoleCapture starts capturing stdout to filter console messages
1443func (v *WebView) startConsoleCapture() {
1444 // This is a simplified approach - we'll rely on the existing OutputCapture
1445 // mechanism in internal/logging/capture.go to handle stdout capture
1446 // and filter for console messages in the log processing
1447}
1448
1449// NewWebViewWithRelated creates a new WebView related to an existing one for process sharing
1450func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) {
1451 if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil {
1452 // Fallback to regular WebView creation
1453 return NewWebView(cfg)
1454 }
1455
1456 log.Printf("[webkit] Creating related WebView for process sharing (CGO)")
1457 if C.gtk_init_check() == 0 {
1458 return nil, errors.New("failed to initialize GTK")
1459 }
1460
1461 if cfg == nil {
1462 cfg = &Config{}
1463 }
1464
1465 // Create WebView related to the existing one
1466 var createdUcm *C.WebKitUserContentManager
1467 viewWidget := C.new_webview_related(relatedView.native.wv, &createdUcm)
1468 if viewWidget == nil {
1469 return nil, errors.New("failed to create related WebKitWebView")
1470 }
1471
1472 var win *C.GtkWidget
1473 var window *Window
1474 container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0)
1475 if container == nil {
1476 return nil, errors.New("failed to create GtkBox container")
1477 }
1478
1479 C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget)
1480
1481 // Make the WebView widget visible and expandable for popup rendering
1482 C.gtk_widget_set_visible(viewWidget, C.gboolean(1))
1483 C.gtk_widget_set_visible(container, C.gboolean(1))
1484 C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1))
1485 C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1))
1486 C.gtk_widget_set_hexpand(container, C.gboolean(1))
1487 C.gtk_widget_set_vexpand(container, C.gboolean(1))
1488 // Remove any size constraints from popup WebView to let it fill the pane
1489 C.gtk_widget_set_size_request(viewWidget, -1, -1)
1490 C.gtk_widget_set_size_request(container, -1, -1)
1491
1492 if cfg.CreateWindow {
1493 win = C.new_window()
1494 if win == nil {
1495 return nil, errors.New("failed to create native window")
1496 }
1497 C.connect_destroy_quit(win)
1498 C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container)
1499 window = &Window{win: win}
1500 } else {
1501 window = &Window{}
1502 }
1503
1504 v := &WebView{
1505 config: cfg,
1506 window: window,
1507 zoom: cfg.ZoomDefault,
1508 native: &nativeView{
1509 win: win,
1510 container: container,
1511 view: viewWidget,
1512 wv: (*C.WebKitWebView)(unsafe.Pointer(viewWidget)),
1513 ucm: createdUcm,
1514 },
1515 useDomZoom: cfg.UseDomZoom,
1516 domZoomSeed: cfg.ZoomDefault,
1517 gcDone: make(chan struct{}),
1518 tlsExceptions: make(map[string]bool),
1519 }
1520
1521 // Enable JavaScript popup windows for workspace popup handling
1522 settings := C.webkit_web_view_get_settings(v.native.wv)
1523 if settings != nil {
1524 C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1))
1525 log.Printf("[webkit] Enabled JavaScript popup windows for related WebView")
1526 }
1527
1528 // Assign an ID for accelerator dispatch
1529 v.id = nextViewID()
1530 registerView(v.id, v)
1531 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1532 ConnectCreateSignal(v) // Connect create signal for window.open support
1533
1534 // Register with memory manager if enabled
1535 if cfg.Memory.EnableMemoryMonitoring {
1536 if globalMemoryManager == nil {
1537 InitializeGlobalMemoryManager(
1538 cfg.Memory.EnableMemoryMonitoring,
1539 cfg.Memory.ProcessRecycleThreshold,
1540 30*time.Second,
1541 )
1542 }
1543 if globalMemoryManager != nil {
1544 globalMemoryManager.RegisterWebView(v)
1545 }
1546 }
1547
1548 if cfg.ZoomDefault <= 0 {
1549 v.zoom = 1.0
1550 }
1551 v.domZoomSeed = v.zoom
1552
1553 // Initialize UCM scripts and handlers (same as NewWebView)
1554 if v.native != nil && v.native.ucm != nil {
1555 v.enableUserContentManager(cfg)
1556 }
1557
1558 return v, nil
1559}
1560
1561func (w *WebView) LoadURL(rawURL string) error {
1562 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1563 return ErrNotImplemented
1564 }
1565
1566 // Track page loads for process recycling
1567 if w.memStats != nil {
1568 w.memStats.pageLoadCount++
1569
1570 // Check if we need to recycle this WebView
1571 if w.config.Memory.ProcessRecycleThreshold > 0 &&
1572 w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
1573 if w.config.Memory.EnableMemoryMonitoring {
1574 log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
1575 w.memStats.pageLoadCount)
1576 }
1577 }
1578 }
1579
1580 w.url = rawURL
1581 curl := C.CString(rawURL)
1582 defer C.free(unsafe.Pointer(curl))
1583 C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
1584 log.Printf("[webkit] LoadURL: %s", rawURL)
1585 return nil
1586}
1587
1588func (w *WebView) Show() error {
1589 if w == nil || w.destroyed || w.native == nil {
1590 return ErrNotImplemented
1591 }
1592 if w.native.container != nil {
1593 C.gtk_widget_set_visible(w.native.container, C.gboolean(1))
1594 }
1595 if w.native.view != nil {
1596 C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
1597 }
1598 if w.native.win == nil {
1599 return ErrNotImplemented
1600 }
1601 C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
1602 C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1603 log.Printf("[webkit] Show window")
1604 return nil
1605}
1606
1607func (w *WebView) Hide() error {
1608 if w == nil || w.destroyed || w.native == nil {
1609 return ErrNotImplemented
1610 }
1611 if w.native.container != nil {
1612 C.gtk_widget_set_visible(w.native.container, C.gboolean(0))
1613 }
1614 if w.native.view != nil {
1615 C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
1616 }
1617 if w.native.win == nil {
1618 return ErrNotImplemented
1619 }
1620 C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
1621 return nil
1622}
1623
1624func (w *WebView) Destroy() error {
1625 if w == nil || w.native == nil {
1626 return ErrNotImplemented
1627 }
1628
1629 // Stop periodic GC if running
1630 w.stopPeriodicGC()
1631
1632 // Clean up memory pressure settings
1633 if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
1634 // WebKit will clean up the memory pressure settings when context is destroyed
1635 w.memStats.memoryPressureSettings = nil
1636 }
1637
1638 // Unregister from memory manager
1639 if globalMemoryManager != nil {
1640 globalMemoryManager.UnregisterWebView(w.id)
1641 }
1642
1643 if w.native != nil && w.native.ucm != nil && w.domBridgeScript != nil {
1644 C.webkit_user_content_manager_remove_script(w.native.ucm, w.domBridgeScript)
1645 C.webkit_user_script_unref(w.domBridgeScript)
1646 w.domBridgeScript = nil
1647 }
1648
1649 // GTK4: destroy windows via gtk_window_destroy if we created one
1650 if w.native.win != nil && w.config != nil && w.config.CreateWindow {
1651 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1652 w.native.win = nil
1653 if w.window != nil {
1654 w.window.win = nil
1655 }
1656 log.Printf("[webkit] Destroy window")
1657 }
1658
1659 w.releaseNativeWidgets()
1660 w.destroyed = true
1661
1662 // Unregister from dynamic protection updates
1663 UnregisterActiveWebView(w)
1664
1665 unregisterView(w.id)
1666 return nil
1667}
1668
1669func (w *WebView) releaseNativeWidgets() {
1670 if w == nil || w.native == nil {
1671 return
1672 }
1673 if w.native.container != nil {
1674 containerPtr := uintptr(unsafe.Pointer(w.native.container))
1675 if containerPtr != 0 && widgetIsValid(containerPtr) {
1676 C.g_object_unref(C.gpointer(w.native.container))
1677 } else {
1678 log.Printf("[webkit] skipping container unref; widget invalid=%#x", containerPtr)
1679 }
1680 w.native.container = nil
1681 }
1682}
1683
1684// Window returns the associated native window.
1685func (w *WebView) Window() *Window { return w.window }
1686
1687// GetCurrentURL returns the current URI from WebKit.
1688func (w *WebView) GetCurrentURL() string {
1689 if w == nil || w.native == nil || w.native.wv == nil {
1690 return ""
1691 }
1692 uri := C.webkit_web_view_get_uri(w.native.wv)
1693 if uri == nil {
1694 return ""
1695 }
1696 return C.GoString((*C.char)(unsafe.Pointer(uri)))
1697}
1698
1699func (w *WebView) GoBack() error {
1700 if w == nil || w.native == nil || w.native.wv == nil {
1701 return ErrNotImplemented
1702 }
1703 C.webkit_web_view_go_back(w.native.wv)
1704 return nil
1705}
1706
1707func (w *WebView) GoForward() error {
1708 if w == nil || w.native == nil || w.native.wv == nil {
1709 return ErrNotImplemented
1710 }
1711 C.webkit_web_view_go_forward(w.native.wv)
1712 return nil
1713}
1714
1715func (w *WebView) Reload() error {
1716 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1717 return ErrNotImplemented
1718 }
1719 C.webkit_web_view_reload(w.native.wv)
1720 log.Printf("[webkit] Reload page")
1721 return nil
1722}
1723
1724func (w *WebView) ReloadBypassCache() error {
1725 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1726 return ErrNotImplemented
1727 }
1728 C.webkit_web_view_reload_bypass_cache(w.native.wv)
1729 log.Printf("[webkit] Hard reload page (bypass cache)")
1730 return nil
1731}
1732
1733// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1734func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1735
1736func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb }
1737
1738func (w *WebView) RegisterCloseHandler(cb func()) {
1739 w.closeHandler = cb
1740 // Also connect the CGO close signal to ensure window.close() is detected
1741 if w.native != nil && w.native.wv != nil {
1742 C.connect_close_signal(w.native.wv, C.ulong(w.id))
1743 }
1744}
1745
1746// EnablePrintInterception connects the print signal to intercept Ctrl+P
1747func (w *WebView) EnablePrintInterception() {
1748 if w.native != nil && w.native.wv != nil {
1749 C.connect_print_signal(w.native.wv, C.ulong(w.id))
1750 }
1751}
1752
1753func (w *WebView) dispatchScriptMessage(payload string) {
1754 if w != nil && w.msgHandler != nil {
1755 w.msgHandler(payload)
1756 }
1757}
1758
1759func (w *WebView) dispatchPopupRequest(uri string) *WebView {
1760 if w != nil && w.popupHandler != nil {
1761 return w.popupHandler(uri)
1762 }
1763 return nil
1764}
1765
1766// CreateRelatedView creates a WebView that shares process/context with this view
1767func (w *WebView) CreateRelatedView() *WebView {
1768 if w == nil || w.native == nil || w.native.wv == nil {
1769 return nil
1770 }
1771 // Build a minimal config inheriting from current view
1772 cfg := &Config{
1773 Assets: w.config.Assets,
1774 ZoomDefault: w.config.ZoomDefault,
1775 EnableDeveloperExtras: w.config.EnableDeveloperExtras,
1776 DataDir: w.config.DataDir,
1777 CacheDir: w.config.CacheDir,
1778 APIToken: w.config.APIToken,
1779 DefaultSansFont: w.config.DefaultSansFont,
1780 DefaultSerifFont: w.config.DefaultSerifFont,
1781 DefaultMonospaceFont: w.config.DefaultMonospaceFont,
1782 DefaultFontSize: w.config.DefaultFontSize,
1783 Rendering: w.config.Rendering,
1784 UseDomZoom: w.config.UseDomZoom,
1785 VideoAcceleration: w.config.VideoAcceleration,
1786 Memory: w.config.Memory,
1787 CodecPreferences: w.config.CodecPreferences,
1788 CreateWindow: false,
1789 }
1790 related, err := NewWebViewWithRelated(cfg, w)
1791 if err != nil {
1792 log.Printf("[webkit] CreateRelatedView failed: %v", err)
1793 return nil
1794 }
1795 return related
1796}
1797
1798// OnWindowTypeDetected registers a callback invoked when window type is detected
1799func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
1800 if w == nil {
1801 return
1802 }
1803 w.windowTypeCallback = callback
1804}
1805
1806// GetNativePointer returns the native WebView pointer for CGO callbacks
1807func (w *WebView) GetNativePointer() unsafe.Pointer {
1808 if w != nil && w.native != nil && w.native.wv != nil {
1809 return unsafe.Pointer(w.native.wv)
1810 }
1811 return nil
1812}
1813
1814// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
1815func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
1816
1817func (w *WebView) dispatchTitleChanged(title string) {
1818 if w != nil && w.titleHandler != nil {
1819 w.titleHandler(title)
1820 }
1821}
1822
1823// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
1824func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
1825
1826func (w *WebView) dispatchURIChanged(uri string) {
1827 if w != nil && w.uriHandler != nil {
1828 w.uriHandler(uri)
1829 }
1830}
1831
1832// RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes.
1833func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) {
1834 if w != nil {
1835 w.faviconHandler = cb
1836 }
1837}
1838
1839// RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI.
1840func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) {
1841 if w != nil {
1842 w.faviconURIHandler = cb
1843 }
1844}
1845
1846func (w *WebView) dispatchFaviconChanged(data []byte) {
1847 if w != nil && w.faviconHandler != nil {
1848 w.faviconHandler(data)
1849 }
1850}
1851
1852func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) {
1853 log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI)
1854 if w != nil && w.faviconURIHandler != nil {
1855 w.faviconURIHandler(pageURI, faviconURI)
1856 }
1857}
1858
1859// RunOnMainThread schedules fn to execute on the GTK main thread.
1860func (w *WebView) RunOnMainThread(fn func()) {
1861 if w == nil || fn == nil {
1862 return
1863 }
1864 h := cgo.NewHandle(fn)
1865 C.schedule_on_main_thread(C.uintptr_t(h))
1866}
1867
1868// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
1869func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
1870
1871func (w *WebView) dispatchZoomChanged(level float64) {
1872 if w != nil && w.zoomHandler != nil {
1873 w.zoomHandler(level)
1874 }
1875}
1876
1877// Widget returns the underlying GtkWidget pointer for the WebView.
1878func (w *WebView) Widget() uintptr {
1879 if w == nil || w.native == nil || w.native.view == nil {
1880 return 0
1881 }
1882 return uintptr(unsafe.Pointer(w.native.view))
1883}
1884
1885// RootWidget returns the container widget that should be reparented for pane management.
1886func (w *WebView) RootWidget() uintptr {
1887 if w == nil || w.native == nil || w.native.container == nil {
1888 return 0
1889 }
1890 return uintptr(unsafe.Pointer(w.native.container))
1891}
1892
1893// DestroyWindow destroys the toplevel window hosting this WebView, if any.
1894func (w *WebView) DestroyWindow() {
1895 if w == nil || w.native == nil || w.native.win == nil {
1896 return
1897 }
1898 // Only destroy if we actually created a window
1899 if w.config != nil && w.config.CreateWindow {
1900 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1901 w.native.win = nil
1902 if w.window != nil {
1903 w.window.win = nil
1904 }
1905 }
1906}
1907
1908// PrepareForReparenting prepares the WebView widget for being moved to a new parent
1909func (w *WebView) PrepareForReparenting() {
1910 if w == nil || w.native == nil || w.native.container == nil {
1911 return
1912 }
1913
1914 // Mark that this view is being reparented
1915 w.wasReparented = true
1916
1917 // Ensure the widget is visible and properly sized for reparenting
1918 C.gtk_widget_set_visible(w.native.container, C.gboolean(1))
1919 C.gtk_widget_set_size_request(w.native.container, 100, 100)
1920 C.gtk_widget_queue_draw(w.native.container)
1921}
1922
1923// RefreshAfterReparenting refreshes the WebView after it has been reparented
1924func (w *WebView) RefreshAfterReparenting() {
1925 if w == nil || w.native == nil || w.native.wv == nil {
1926 return
1927 }
1928
1929 if w.wasReparented {
1930 widgetHandle := w.RootWidget()
1931 parentHandle := WidgetGetParent(widgetHandle)
1932 if parentHandle == 0 {
1933 log.Printf("[workspace] RefreshAfterReparenting deferred: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url)
1934 return
1935 }
1936 log.Printf("[workspace] RefreshAfterReparenting: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url)
1937 // Force WebKit to refresh its rendering context by triggering a reload
1938 // This ensures the rendering context is properly reinitialized after reparenting
1939 if w.url != "" {
1940 w.LoadURL(w.url) // Reload current URL to refresh rendering context
1941 }
1942 w.wasReparented = false
1943 }
1944}
1945
1946// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1947func (w *WebView) enableUserContentManager(cfg *Config) {
1948 if w == nil || w.native == nil || w.native.ucm == nil {
1949 return
1950 }
1951 // Register handler "dumber" for main world
1952 cname := C.CString("dumber")
1953 ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
1954 if ok == 0 {
1955 log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
1956 }
1957 C.free(unsafe.Pointer(cname))
1958
1959 // Also register handler for the isolated GUI world so GUI can post messages to native
1960 cworld := C.CString("dumber-gui")
1961 cname2 := C.CString("dumber")
1962 ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id))
1963 if ok2 == 0 {
1964 log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'")
1965 }
1966 C.free(unsafe.Pointer(cworld))
1967 C.free(unsafe.Pointer(cname2))
1968
1969 // Inject color-scheme preference script at document-start to inform sites of system theme
1970 preferDark := C.gtk_prefers_dark() != 0
1971 if preferDark {
1972 log.Printf("[theme] GTK prefers: dark")
1973 } else {
1974 log.Printf("[theme] GTK prefers: light")
1975 }
1976 // Load color scheme script from compiled GUI assets instead of inline
1977 var schemeJS string
1978 if cfg != nil && cfg.Assets != nil {
1979 if schemeBytes, err := cfg.Assets.ReadFile("assets/gui/color-scheme.js"); err == nil {
1980 baseScript := string(schemeBytes)
1981 // Inject the GTK preference as a parameter
1982 if preferDark {
1983 schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = true; %s", baseScript)
1984 } else {
1985 schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = false; %s", baseScript)
1986 }
1987 } else {
1988 log.Printf("[webkit] Warning: Failed to load color-scheme.js from assets, falling back to basic theme detection: %v", err)
1989 // Simple fallback if the compiled script is not available
1990 if preferDark {
1991 schemeJS = `console.log('[dumber] GTK detected color mode: dark'); document.documentElement.classList.add('dark');`
1992 } else {
1993 schemeJS = `console.log('[dumber] GTK detected color mode: light'); document.documentElement.classList.remove('dark');`
1994 }
1995 }
1996 }
1997 cScheme := C.CString(schemeJS)
1998 defer C.free(unsafe.Pointer(cScheme))
1999 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)
2000 if schemeScript != nil {
2001 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
2002 C.webkit_user_script_unref(schemeScript)
2003 }
2004
2005 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
2006 if cfg != nil && cfg.Assets != nil {
2007 log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
2008 if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
2009 guiScript := string(guiBytes)
2010 log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
2011
2012 cGui := C.CString(guiScript)
2013 defer C.free(unsafe.Pointer(cGui))
2014
2015 log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'")
2016 world := C.CString("dumber-gui")
2017 defer C.free(unsafe.Pointer(world))
2018 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)
2019 if guiUserScript != nil {
2020 C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
2021 C.webkit_user_script_unref(guiUserScript)
2022 log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world")
2023 } else {
2024 log.Printf("[webkit] ERROR: Failed to create GUI user script")
2025 }
2026
2027 // Inject a unified page-world bridge to dispatch standard events and expose unified API
2028 w.installDomBridgeScript()
2029
2030 // If an API token is provided, inject a fetch wrapper in the isolated GUI world
2031 if cfg.APIToken != "" {
2032 wrapper := "(() => { try {" +
2033 "const TOKEN='" + cfg.APIToken + "';" +
2034 "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; };" +
2035 "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); };" +
2036 "} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();"
2037 cWrap := C.CString(wrapper)
2038 defer C.free(unsafe.Pointer(cWrap))
2039 world := C.CString("dumber-gui")
2040 defer C.free(unsafe.Pointer(world))
2041 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)
2042 if wrapScript != nil {
2043 C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript)
2044 C.webkit_user_script_unref(wrapScript)
2045 log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world")
2046 }
2047 }
2048 } else {
2049 log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
2050 }
2051
2052 // Add GUI CSS styles as user stylesheet when a legacy bundle is present.
2053 // Modern builds inline Tailwind output inside gui.min.js, so this file is optional.
2054 if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
2055 cssContent := string(cssBytes)
2056 cCss := C.CString(cssContent)
2057 defer C.free(unsafe.Pointer(cCss))
2058
2059 styleSheet := C.webkit_user_style_sheet_new(
2060 (*C.gchar)(cCss),
2061 C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
2062 C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
2063 nil, // allow_list
2064 nil, // block_list
2065 )
2066 if styleSheet != nil {
2067 C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
2068 C.webkit_user_style_sheet_unref(styleSheet)
2069 } else {
2070 log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
2071 }
2072 } else if errors.Is(err, os.ErrNotExist) {
2073 log.Printf("[webkit] GUI stylesheet not found; assuming inline Tailwind bundle and skipping separate injection")
2074 } else {
2075 log.Printf("[webkit] Warning: Failed to load GUI CSS from assets, skipping separate stylesheet injection: %v", err)
2076 }
2077 }
2078
2079 // Inject Wails runtime fetch interceptor for homepage bridging
2080 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(_){} })();`
2081 cWails := C.CString(wailsBridge)
2082 defer C.free(unsafe.Pointer(cWails))
2083 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)
2084 if wailsScript != nil {
2085 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
2086 C.webkit_user_script_unref(wailsScript)
2087 }
2088
2089 // Inject codec control script if codec preferences are configured
2090 if cfg != nil && (cfg.CodecPreferences.ForceAV1 || len(cfg.CodecPreferences.BlockedCodecs) > 0 || len(cfg.CodecPreferences.PreferredCodecs) > 0) {
2091 codecScript := GenerateCodecControlScript(cfg.CodecPreferences)
2092 cCodec := C.CString(codecScript)
2093 defer C.free(unsafe.Pointer(cCodec))
2094 codecUserScript := C.webkit_user_script_new(
2095 (*C.gchar)(cCodec),
2096 C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
2097 C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
2098 nil, nil)
2099 if codecUserScript != nil {
2100 C.webkit_user_content_manager_add_script(w.native.ucm, codecUserScript)
2101 C.webkit_user_script_unref(codecUserScript)
2102 log.Printf("[webkit] Codec control script injected")
2103 }
2104 }
2105
2106 // Enable global shortcuts protection to prevent shortcuts from leaking to webpages
2107 if err := w.EnableGlobalShortcutsProtection(); err != nil {
2108 log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)
2109 }
2110
2111 // Register this WebView for dynamic protection updates
2112 RegisterActiveWebView(w)
2113
2114 // No JS fallback bridge — native UCM is active
2115}
2116
2117func clampDomZoom(level float64) float64 {
2118 if level < 0.25 {
2119 return 0.25
2120 }
2121 if level > 5.0 {
2122 return 5.0
2123 }
2124 return level
2125}
2126
2127func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w *WebView) string {
2128 if cfg == nil || cfg.Assets == nil {
2129 log.Printf("[webkit] Config or assets not available for main-world script")
2130 return ""
2131 }
2132
2133 // Load the compiled main-world script
2134 scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js")
2135 if err != nil {
2136 log.Printf("[webkit] Failed to load main-world script: %v", err)
2137 return ""
2138 }
2139
2140 // Replace the zoom placeholder with the actual value
2141 zoom := clampDomZoom(initialZoom)
2142 zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64)
2143 script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr)
2144
2145 // Inject WebView ID and active state
2146 webViewIdStr := strconv.FormatUint(uint64(webViewId), 10)
2147 script = strings.ReplaceAll(script, "__WEBVIEW_ID__", webViewIdStr)
2148 // Default to active for the initial WebView, since it will be focused immediately
2149 // For single WebView scenarios, assume it should be active
2150 activeState := "true"
2151 log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)
2152 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
2153
2154 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2155 return script
2156}
2157
2158func (w *WebView) UsesDomZoom() bool {
2159 return w != nil && w.useDomZoom
2160}
2161
2162func (w *WebView) installDomBridgeScript() {
2163 if w == nil || w.native == nil || w.native.ucm == nil {
2164 return
2165 }
2166
2167 w.domZoomSeed = clampDomZoom(w.domZoomSeed)
2168
2169 if w.domBridgeScript != nil {
2170 C.webkit_user_content_manager_remove_script(w.native.ucm, w.domBridgeScript)
2171 C.webkit_user_script_unref(w.domBridgeScript)
2172 w.domBridgeScript = nil
2173 }
2174
2175 source := buildDomBridgeScript(w.config, w.domZoomSeed, w.id, w)
2176 cBridge := C.CString(source)
2177 defer C.free(unsafe.Pointer(cBridge))
2178
2179 script := 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)
2180 if script == nil {
2181 log.Printf("[webkit] ERROR: Failed to create GUI user script")
2182 return
2183 }
2184
2185 C.webkit_user_content_manager_add_script(w.native.ucm, script)
2186 w.domBridgeScript = script
2187 log.Printf("[webkit] Page-world toast bridge injected (dom zoom seed=%.2f)", w.domZoomSeed)
2188}
2189
2190// SeedDomZoom updates the document-start bridge so the upcoming page paints with the saved DOM zoom level.
2191func (w *WebView) SeedDomZoom(level float64) {
2192 if w == nil || w.destroyed || !w.useDomZoom {
2193 return
2194 }
2195
2196 seed := clampDomZoom(level)
2197 if math.Abs(w.domZoomSeed-seed) < 1e-6 {
2198 return
2199 }
2200 w.domZoomSeed = seed
2201 // Instead of re-injecting the entire script, just update the zoom level
2202 w.RunOnMainThread(func() {
2203 zoomStr := strconv.FormatFloat(seed, 'f', -1, 64)
2204 script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr)
2205 w.InjectScript(script)
2206 })
2207}
2208
2209// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
2210func (w *WebView) startPeriodicGC(interval time.Duration) {
2211 if w == nil || w.gcTicker != nil {
2212 return // Already running or invalid
2213 }
2214
2215 w.gcTicker = time.NewTicker(interval)
2216
2217 go func() {
2218 defer func() {
2219 if w.gcTicker != nil {
2220 w.gcTicker.Stop()
2221 w.gcTicker = nil
2222 }
2223 }()
2224
2225 for {
2226 select {
2227 case <-w.gcTicker.C:
2228 w.triggerGC()
2229 case <-w.gcDone:
2230 return
2231 }
2232 }
2233 }()
2234}
2235
2236// stopPeriodicGC stops the periodic garbage collection
2237func (w *WebView) stopPeriodicGC() {
2238 if w == nil {
2239 return
2240 }
2241
2242 if w.gcDone != nil {
2243 close(w.gcDone)
2244 w.gcDone = nil
2245 }
2246
2247 if w.gcTicker != nil {
2248 w.gcTicker.Stop()
2249 w.gcTicker = nil
2250 }
2251}
2252
2253// triggerGC manually triggers JavaScript garbage collection
2254func (w *WebView) triggerGC() {
2255 if w == nil || w.memStats == nil {
2256 return
2257 }
2258
2259 // Get default WebContext and trigger GC
2260 ctx := C.webkit_web_context_get_default()
2261 if ctx != nil {
2262 C.maybe_collect_js(ctx)
2263 w.memStats.lastGCTime = time.Now()
2264
2265 if w.config.Memory.EnableMemoryMonitoring {
2266 log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
2267 }
2268 }
2269}
2270
2271// GetMemoryStats returns current memory statistics for this WebView
2272func (w *WebView) GetMemoryStats() map[string]interface{} {
2273 if w == nil || w.memStats == nil {
2274 return nil
2275 }
2276
2277 return map[string]interface{}{
2278 "page_load_count": w.memStats.pageLoadCount,
2279 "last_gc_time": w.memStats.lastGCTime,
2280 "has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
2281 }
2282}
2283
2284// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
2285func (w *WebView) TriggerMemoryCleanup() {
2286 if w == nil {
2287 return
2288 }
2289
2290 w.triggerGC()
2291
2292 if w.config.Memory.EnableMemoryMonitoring {
2293 log.Printf("[webkit] Manual memory cleanup triggered")
2294 }
2295}
2296
2297//export goHandleTLSError
2298func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
2299 uri := C.GoString(failingURI)
2300 hostname := C.GoString(host)
2301 certificateInfo := C.GoString(certInfo)
2302
2303 log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
2304
2305 // Find the WebView that triggered this error
2306 // For now, we'll get the most recently created view
2307 // In a more sophisticated implementation, we'd pass the view ID through user_data
2308 var webView *WebView
2309 regMu.RLock()
2310 for _, v := range viewByID {
2311 if v != nil {
2312 webView = v
2313 break
2314 }
2315 }
2316 regMu.RUnlock()
2317
2318 if webView == nil {
2319 log.Printf("[tls] No WebView found to handle TLS error")
2320 return C.FALSE // Don't proceed
2321 }
2322
2323 // Check if we've already allowed this host
2324 if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
2325 log.Printf("[tls] Certificate exception already granted for %s", hostname)
2326 return C.TRUE // Proceed with the load
2327 }
2328
2329 // Show warning dialog and get user decision
2330 if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
2331 webView.tlsExceptions[hostname] = true
2332 log.Printf("[tls] User accepted certificate exception for %s", hostname)
2333 return C.TRUE // Proceed with the load
2334 }
2335
2336 log.Printf("[tls] User rejected certificate for %s", hostname)
2337 return C.FALSE // Don't proceed
2338}
2339
2340//export goHandleConsoleMessage
2341//export goHandleConsoleMessage
2342func goHandleConsoleMessage(message *C.char) {
2343 messageText := C.GoString(message)
2344 logging.CaptureWebKitLog(messageText)
2345}
2346
2347// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
2348func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
2349 if w == nil || w.native == nil || w.native.win == nil {
2350 log.Printf("[tls] Cannot show dialog: WebView or window is nil")
2351 return false
2352 }
2353
2354 // Check if we have a stored decision for this certificate
2355 if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
2356 switch storedDecision {
2357 case "accepted":
2358 log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
2359 return true
2360 case "rejected":
2361 log.Printf("[tls] Using stored REJECT decision for %s", hostname)
2362 return false
2363 default:
2364 log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
2365 }
2366 }
2367
2368 log.Printf("[tls] Showing certificate error dialog for %s", hostname)
2369
2370 // Format the error message with certificate information
2371 errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
2372
2373 // Convert strings to C strings
2374 cHostname := C.CString(hostname)
2375 defer C.free(unsafe.Pointer(cHostname))
2376
2377 cErrorMsg := C.CString(errorMsg)
2378 defer C.free(unsafe.Pointer(cErrorMsg))
2379
2380 // Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
2381 result := C.show_tls_warning_dialog_sync(
2382 (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
2383 cHostname,
2384 cErrorMsg,
2385 )
2386
2387 responseCode := int(result)
2388
2389 switch responseCode {
2390 case 0: // Go Back
2391 log.Printf("[tls] User chose to GO BACK for %s", hostname)
2392 return false
2393
2394 case 1: // Proceed Once
2395 log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
2396 // Store temporary decision (expires in 24 hours)
2397 if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
2398 log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
2399 }
2400 return true
2401
2402 case 2: // Always Accept
2403 log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
2404 // Store permanent decision
2405 if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
2406 log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
2407 }
2408 return true
2409
2410 default:
2411 log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
2412 return false
2413 }
2414}
2415
2416// formatTLSErrorMessage creates a detailed error message based on the error flags
2417func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
2418 msg := "Website: " + hostname + "\n\n"
2419
2420 // Add certificate information
2421 if certificateInfo != "" {
2422 msg += "Certificate Details:\n" + certificateInfo + "\n"
2423 }
2424
2425 msg += "Security Issues:\n"
2426 // Decode GTlsCertificateFlags (common values)
2427 if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
2428 msg += "• Certificate authority is not trusted\n"
2429 }
2430 if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
2431 msg += "• Certificate does not match the website identity\n"
2432 }
2433 if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
2434 msg += "• Certificate is not yet valid\n"
2435 }
2436 if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
2437 msg += "• Certificate has expired\n"
2438 }
2439 if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
2440 msg += "• Certificate has been revoked\n"
2441 }
2442 if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
2443 msg += "• Certificate uses weak cryptography\n"
2444 }
2445
2446 msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
2447
2448 return msg
2449}
2450
2451// InitializeContentBlocking initializes WebKit content blocking with filter manager
2452func (w *WebView) InitializeContentBlocking(filterManager FilterManager) error {
2453 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
2454 return ErrWebViewNotInitialized
2455 }
2456
2457 // Load filters asynchronously with proper error recovery
2458 go func() {
2459 defer func() {
2460 if r := recover(); r != nil {
2461 logging.Error(fmt.Sprintf("[webkit] Content blocking initialization panic: %v", r))
2462 }
2463 }()
2464
2465 // Wait longer for WebView to be ready before applying filters on first load
2466 // This prevents interference with WebKit's preconnect operations
2467 time.Sleep(1500 * time.Millisecond)
2468
2469 // Retry mechanism with exponential backoff
2470 maxRetries := 5
2471 baseDelay := 200 * time.Millisecond
2472
2473 // Apply network filters with error recovery
2474 for i := 0; i < maxRetries; i++ {
2475 if w.destroyed {
2476 logging.Debug("[webkit] WebView destroyed, aborting filter loading")
2477 return
2478 }
2479
2480 if filters, err := filterManager.GetNetworkFilters(); err == nil {
2481 if len(filters) > 10 && string(filters) != "null" {
2482 logging.Info("[webkit] Loading network filters from filter manager (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
2483 if err := w.ApplyContentFilters(filters, "network-filters"); err != nil {
2484 logging.Error("Failed to apply network filters (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
2485 if i == maxRetries-1 {
2486 logging.Warn("[webkit] Network filtering disabled due to errors")
2487 break
2488 }
2489 time.Sleep(baseDelay * time.Duration(1<<uint(i))) // Exponential backoff
2490 continue
2491 }
2492 break
2493 }
2494 } else if i == maxRetries-1 {
2495 logging.Error("Failed to get network filters after retries: " + err.Error())
2496 }
2497 }
2498
2499 // Apply cosmetic filtering with error recovery
2500 time.Sleep(200 * time.Millisecond) // Additional delay to ensure network setup is complete
2501
2502 for i := 0; i < maxRetries; i++ {
2503 if w.destroyed {
2504 logging.Debug("[webkit] WebView destroyed, aborting cosmetic filter loading")
2505 return
2506 }
2507
2508 script := filterManager.GetCosmeticScript()
2509 if len(script) > 0 {
2510 logging.Info("[webkit] Loading cosmetic script from filter manager (" + fmt.Sprintf("%d", len(script)) + " chars)")
2511 if err := w.InjectCosmeticFilter(script); err != nil {
2512 logging.Error("Failed to inject cosmetic filter (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
2513 if i == maxRetries-1 {
2514 logging.Warn("[webkit] Cosmetic filtering disabled due to errors")
2515 } else {
2516 time.Sleep(baseDelay * time.Duration(1<<uint(i)))
2517 continue
2518 }
2519 }
2520 break
2521 }
2522 }
2523
2524 logging.Info("Content blocking initialization completed")
2525 }()
2526
2527 return nil
2528}
2529
2530// OnNavigate sets up domain-specific cosmetic filtering on navigation
2531func (w *WebView) OnNavigate(url string, filterManager FilterManager) {
2532 if w == nil || w.destroyed {
2533 return
2534 }
2535
2536 domain := extractDomain(url)
2537 if domain == "" {
2538 return
2539 }
2540
2541 // Get domain-specific cosmetic rules
2542 script := filterManager.GetCosmeticScriptForDomain(domain)
2543 if script != "" {
2544 // Inject domain-specific cosmetic rules
2545 logging.Info("[webkit] Applying domain-specific cosmetic rules for: " + domain + " (" + fmt.Sprintf("%d", len(script)) + " chars)")
2546 w.InjectScript(fmt.Sprintf(
2547 "if (typeof window.__dumber_cosmetic_init === 'function') { %s }",
2548 script,
2549 ))
2550 } else {
2551 logging.Debug("[webkit] No domain-specific cosmetic rules found for: " + domain)
2552 }
2553}
2554
2555// UpdateContentFilters updates the content filters dynamically
2556func (w *WebView) UpdateContentFilters(filterManager FilterManager) error {
2557 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
2558 return ErrWebViewNotInitialized
2559 }
2560
2561 // Clear existing filters first
2562 if err := w.ClearAllFilters(); err != nil {
2563 return fmt.Errorf("failed to clear existing filters: %w", err)
2564 }
2565
2566 // Apply new filters
2567 if filters, err := filterManager.GetNetworkFilters(); err == nil {
2568 logging.Info("[webkit] Updating network filters (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
2569 if err := w.ApplyContentFilters(filters, "network-filters-updated"); err != nil {
2570 return fmt.Errorf("failed to apply updated network filters: %w", err)
2571 }
2572 }
2573
2574 // Update cosmetic filtering
2575 script := filterManager.GetCosmeticScript()
2576 logging.Info("[webkit] Updating cosmetic script (" + fmt.Sprintf("%d", len(script)) + " chars)")
2577 if err := w.InjectCosmeticFilter(script); err != nil {
2578 return fmt.Errorf("failed to inject updated cosmetic filter: %w", err)
2579 }
2580
2581 logging.Info("Content filters updated successfully")
2582 return nil
2583}
2584
2585// extractDomain extracts the domain from a URL
2586func extractDomain(url string) string {
2587 if url == "" {
2588 return ""
2589 }
2590
2591 // Simple domain extraction - remove protocol and path
2592 start := strings.Index(url, "://")
2593 if start != -1 {
2594 url = url[start+3:]
2595 }
2596
2597 end := strings.Index(url, "/")
2598 if end != -1 {
2599 url = url[:end]
2600 }
2601
2602 // Remove port if present
2603 if portIdx := strings.Index(url, ":"); portIdx != -1 {
2604 url = url[:portIdx]
2605 }
2606
2607 return url
2608}
2609
2610// detectAndLogRenderingBackend detects and logs the actual rendering backend being used
2611func (w *WebView) detectAndLogRenderingBackend() {
2612 if w == nil {
2613 return
2614 }
2615
2616 // Use a small delay to let WebKit initialize
2617 go func() {
2618 time.Sleep(500 * time.Millisecond)
2619
2620 // Inject a script to detect rendering capabilities
2621 detectionScript := `
2622 (() => {
2623 try {
2624 const canvas = document.createElement('canvas');
2625 const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
2626 const info = {
2627 webgl_available: !!gl,
2628 renderer: gl ? gl.getParameter(gl.RENDERER) : 'N/A',
2629 vendor: gl ? gl.getParameter(gl.VENDOR) : 'N/A',
2630 version: gl ? gl.getParameter(gl.VERSION) : 'N/A',
2631 shading_language_version: gl ? gl.getParameter(gl.SHADING_LANGUAGE_VERSION) : 'N/A',
2632 max_texture_size: gl ? gl.getParameter(gl.MAX_TEXTURE_SIZE) : 0,
2633 hardware_accelerated: false
2634 };
2635
2636 // Check if renderer indicates hardware acceleration
2637 if (gl && info.renderer) {
2638 const renderer = info.renderer.toLowerCase();
2639 // Look for GPU indicators
2640 info.hardware_accelerated = (
2641 renderer.includes('nvidia') ||
2642 renderer.includes('amd') ||
2643 renderer.includes('intel') ||
2644 renderer.includes('radeon') ||
2645 renderer.includes('geforce') ||
2646 renderer.includes('quadro') ||
2647 (renderer.includes('mesa') && !renderer.includes('software')) ||
2648 !renderer.includes('software')
2649 );
2650 }
2651
2652 window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({
2653 type: 'rendering_backend_detection',
2654 data: info
2655 }));
2656 } catch (e) {
2657 console.error('[dumber] Rendering backend detection failed:', e);
2658 }
2659 })();
2660 `
2661
2662 w.InjectScript(detectionScript)
2663 }()
2664}
2665
2666// SetWindowFeatures sets the window features for this WebView
2667func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
2668 w.windowFeatures = features
2669}
2670
2671// SetActive sets whether this WebView is currently active/focused
2672func (w *WebView) SetActive(active bool) {
2673 if w == nil || w.destroyed {
2674 return
2675 }
2676
2677 if w.isActive != active {
2678 w.isActive = active
2679 log.Printf("[webkit] WebView %d active state changed to: %v", w.id, active)
2680
2681 // Update the active state in the JavaScript context
2682 activeStr := "false"
2683 if active {
2684 activeStr = "true"
2685 }
2686 script := fmt.Sprintf("window.__dumber_is_active = %s;", activeStr)
2687 w.InjectScript(script)
2688 }
2689}
2690
2691// IsActive returns whether this WebView is currently active/focused
2692func (w *WebView) IsActive() bool {
2693 return w != nil && w.isActive
2694}
2695
2696// IsDestroyed returns whether this WebView has been destroyed
2697func (w *WebView) IsDestroyed() bool {
2698 return w == nil || w.destroyed
2699}
2700
2701// ID returns the unique identifier for this WebView as a string
2702func (w *WebView) ID() string {
2703 if w == nil {
2704 return ""
2705 }
2706 return strconv.FormatUint(uint64(w.id), 10)
2707}
2708
2709// PrefersDarkTheme returns true if GTK is configured to prefer dark theme
2710func PrefersDarkTheme() bool {
2711 return C.gtk_prefers_dark() != 0
2712}