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