using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Microsoft.Maui.Platform.Linux.Services; namespace Microsoft.Maui.Platform.Linux.Native; internal static class WebKitNative { private delegate IntPtr WebKitWebViewNewDelegate(); private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, string uri); private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, string content, string? baseUri); private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView); private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView); private delegate void WebKitWebViewGoBackDelegate(IntPtr webView); private delegate void WebKitWebViewGoForwardDelegate(IntPtr webView); private delegate bool WebKitWebViewCanGoBackDelegate(IntPtr webView); private delegate bool WebKitWebViewCanGoForwardDelegate(IntPtr webView); private delegate void WebKitWebViewReloadDelegate(IntPtr webView); private delegate void WebKitWebViewStopLoadingDelegate(IntPtr webView); private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView); private delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy); private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled); private delegate void WebKitWebViewSetBackgroundColorDelegate(IntPtr webView, ref GdkRGBA color); [StructLayout(LayoutKind.Sequential)] public struct GdkRGBA { public double Red; public double Green; public double Blue; public double Alpha; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate bool ScriptDialogCallback(IntPtr webView, IntPtr dialog, IntPtr userData); private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, Delegate callback, IntPtr userData, IntPtr destroyNotify, int connectFlags); // WebKitScriptDialog functions private delegate int WebKitScriptDialogGetDialogTypeDelegate(IntPtr dialog); private delegate IntPtr WebKitScriptDialogGetMessageDelegate(IntPtr dialog); private delegate void WebKitScriptDialogConfirmSetConfirmedDelegate(IntPtr dialog, bool confirmed); private delegate IntPtr WebKitScriptDialogPromptGetDefaultTextDelegate(IntPtr dialog); private delegate void WebKitScriptDialogPromptSetTextDelegate(IntPtr dialog, string text); public enum WebKitLoadEvent { Started, Redirected, Committed, Finished } public enum WebKitScriptDialogType { Alert = 0, Confirm = 1, Prompt = 2, BeforeUnloadConfirm = 3 } private static IntPtr _handle; private static bool _initialized; private static readonly string[] LibraryNames = new string[4] { "libwebkit2gtk-4.1.so.0", "libwebkit2gtk-4.0.so.37", "libwebkit2gtk-4.0.so", "libwebkit2gtk-4.1.so" }; private static WebKitWebViewNewDelegate? _webkitWebViewNew; private static WebKitWebViewLoadUriDelegate? _webkitLoadUri; private static WebKitWebViewLoadHtmlDelegate? _webkitLoadHtml; private static WebKitWebViewGetUriDelegate? _webkitGetUri; private static WebKitWebViewGetTitleDelegate? _webkitGetTitle; private static WebKitWebViewGoBackDelegate? _webkitGoBack; private static WebKitWebViewGoForwardDelegate? _webkitGoForward; private static WebKitWebViewCanGoBackDelegate? _webkitCanGoBack; private static WebKitWebViewCanGoForwardDelegate? _webkitCanGoForward; private static WebKitWebViewReloadDelegate? _webkitReload; private static WebKitWebViewStopLoadingDelegate? _webkitStopLoading; private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings; private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAccel; private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript; private static WebKitWebViewSetBackgroundColorDelegate? _webkitSetBackgroundColor; private static GSignalConnectDataDelegate? _gSignalConnectData; private static WebKitScriptDialogGetDialogTypeDelegate? _webkitScriptDialogGetDialogType; private static WebKitScriptDialogGetMessageDelegate? _webkitScriptDialogGetMessage; private static WebKitScriptDialogConfirmSetConfirmedDelegate? _webkitScriptDialogConfirmSetConfirmed; private static WebKitScriptDialogPromptGetDefaultTextDelegate? _webkitScriptDialogPromptGetDefaultText; private static WebKitScriptDialogPromptSetTextDelegate? _webkitScriptDialogPromptSetText; private static readonly Dictionary _loadChangedCallbacks = new Dictionary(); private static readonly Dictionary _scriptDialogCallbacks = new Dictionary(); private static readonly Dictionary _loadChangedSignalIds = new Dictionary(); private static readonly Dictionary _scriptDialogSignalIds = new Dictionary(); /// /// Event raised when a JavaScript dialog (alert, confirm, prompt) is requested. /// public static event Action? ScriptDialogRequested; private const int RTLD_NOW = 2; private const int RTLD_GLOBAL = 256; private static IntPtr _gobjectHandle; [DllImport("libdl.so.2")] private static extern IntPtr dlopen(string? filename, int flags); [DllImport("libdl.so.2")] private static extern IntPtr dlsym(IntPtr handle, string symbol); [DllImport("libdl.so.2")] private static extern int dlclose(IntPtr handle); [DllImport("libdl.so.2")] private static extern IntPtr dlerror(); [DllImport("libgobject-2.0.so.0")] private static extern void g_signal_handler_disconnect(IntPtr instance, ulong handlerId); public static bool Initialize() { if (_initialized) { return _handle != IntPtr.Zero; } _initialized = true; string[] libraryNames = LibraryNames; foreach (string text in libraryNames) { _handle = dlopen(text, 258); if (_handle != IntPtr.Zero) { DiagnosticLog.Debug("WebKitNative", "Loaded " + text); break; } } if (_handle == IntPtr.Zero) { DiagnosticLog.Warn("WebKitNative", "Failed to load WebKitGTK library"); return false; } _webkitWebViewNew = LoadFunction("webkit_web_view_new"); _webkitLoadUri = LoadFunction("webkit_web_view_load_uri"); _webkitLoadHtml = LoadFunction("webkit_web_view_load_html"); _webkitGetUri = LoadFunction("webkit_web_view_get_uri"); _webkitGetTitle = LoadFunction("webkit_web_view_get_title"); _webkitGoBack = LoadFunction("webkit_web_view_go_back"); _webkitGoForward = LoadFunction("webkit_web_view_go_forward"); _webkitCanGoBack = LoadFunction("webkit_web_view_can_go_back"); _webkitCanGoForward = LoadFunction("webkit_web_view_can_go_forward"); _webkitReload = LoadFunction("webkit_web_view_reload"); _webkitStopLoading = LoadFunction("webkit_web_view_stop_loading"); _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); _webkitSetHardwareAccel = LoadFunction("webkit_settings_set_hardware_acceleration_policy"); _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); _webkitSetBackgroundColor = LoadFunction("webkit_web_view_set_background_color"); _webkitScriptDialogGetDialogType = LoadFunction("webkit_script_dialog_get_dialog_type"); _webkitScriptDialogGetMessage = LoadFunction("webkit_script_dialog_get_message"); _webkitScriptDialogConfirmSetConfirmed = LoadFunction("webkit_script_dialog_confirm_set_confirmed"); _webkitScriptDialogPromptGetDefaultText = LoadFunction("webkit_script_dialog_prompt_get_default_text"); _webkitScriptDialogPromptSetText = LoadFunction("webkit_script_dialog_prompt_set_text"); _gobjectHandle = dlopen("libgobject-2.0.so.0", 258); if (_gobjectHandle != IntPtr.Zero) { IntPtr intPtr = dlsym(_gobjectHandle, "g_signal_connect_data"); if (intPtr != IntPtr.Zero) { _gSignalConnectData = Marshal.GetDelegateForFunctionPointer(intPtr); DiagnosticLog.Debug("WebKitNative", "Loaded g_signal_connect_data"); } } return _webkitWebViewNew != null; } private static T? LoadFunction(string name) where T : Delegate { if (_handle == IntPtr.Zero) { return null; } IntPtr intPtr = dlsym(_handle, name); if (intPtr == IntPtr.Zero) { return null; } return Marshal.GetDelegateForFunctionPointer(intPtr); } public static IntPtr WebViewNew() { if (!Initialize() || _webkitWebViewNew == null) { return IntPtr.Zero; } return _webkitWebViewNew(); } public static void LoadUri(IntPtr webView, string uri) { _webkitLoadUri?.Invoke(webView, uri); } public static void LoadHtml(IntPtr webView, string content, string? baseUri = null) { _webkitLoadHtml?.Invoke(webView, content, baseUri); } public static string? GetUri(IntPtr webView) { IntPtr intPtr = _webkitGetUri?.Invoke(webView) ?? IntPtr.Zero; if (intPtr == IntPtr.Zero) { return null; } return Marshal.PtrToStringUTF8(intPtr); } public static string? GetTitle(IntPtr webView) { IntPtr intPtr = _webkitGetTitle?.Invoke(webView) ?? IntPtr.Zero; if (intPtr == IntPtr.Zero) { return null; } return Marshal.PtrToStringUTF8(intPtr); } public static void GoBack(IntPtr webView) { _webkitGoBack?.Invoke(webView); } public static void GoForward(IntPtr webView) { _webkitGoForward?.Invoke(webView); } public static bool CanGoBack(IntPtr webView) { return _webkitCanGoBack?.Invoke(webView) ?? false; } public static bool CanGoForward(IntPtr webView) { return _webkitCanGoForward?.Invoke(webView) ?? false; } public static void Reload(IntPtr webView) { _webkitReload?.Invoke(webView); } public static void StopLoading(IntPtr webView) { _webkitStopLoading?.Invoke(webView); } public static void ConfigureSettings(IntPtr webView, bool disableHardwareAccel = true) { if (_webkitGetSettings != null) { IntPtr intPtr = _webkitGetSettings(webView); if (intPtr != IntPtr.Zero && disableHardwareAccel && _webkitSetHardwareAccel != null) { _webkitSetHardwareAccel(intPtr, 2); } } } public static void SetJavascriptEnabled(IntPtr webView, bool enabled) { if (_webkitGetSettings != null && _webkitSetJavascript != null) { IntPtr intPtr = _webkitGetSettings(webView); if (intPtr != IntPtr.Zero) { _webkitSetJavascript(intPtr, enabled); } } } public static void SetBackgroundColor(IntPtr webView, double r, double g, double b, double a = 1.0) { if (_webkitSetBackgroundColor != null && webView != IntPtr.Zero) { var color = new GdkRGBA { Red = r, Green = g, Blue = b, Alpha = a }; _webkitSetBackgroundColor(webView, ref color); } } public static ulong ConnectLoadChanged(IntPtr webView, LoadChangedCallback callback) { if (_gSignalConnectData == null || webView == IntPtr.Zero) { DiagnosticLog.Warn("WebKitNative", "Cannot connect load-changed: signal connect not available"); return 0uL; } _loadChangedCallbacks[webView] = callback; ulong signalId = _gSignalConnectData(webView, "load-changed", callback, IntPtr.Zero, IntPtr.Zero, 0); _loadChangedSignalIds[webView] = signalId; return signalId; } public static void DisconnectLoadChanged(IntPtr webView) { if (_loadChangedSignalIds.TryGetValue(webView, out ulong signalId) && signalId != 0) { g_signal_handler_disconnect(webView, signalId); _loadChangedSignalIds.Remove(webView); } _loadChangedCallbacks.Remove(webView); } /// /// Connects to the script-dialog signal to intercept JavaScript alert/confirm/prompt dialogs. /// Returns true from the callback to prevent the default WebKitGTK dialog. /// public static ulong ConnectScriptDialog(IntPtr webView, ScriptDialogCallback callback) { if (_gSignalConnectData == null || webView == IntPtr.Zero) { DiagnosticLog.Warn("WebKitNative", "Cannot connect script-dialog: signal connect not available"); return 0uL; } _scriptDialogCallbacks[webView] = callback; ulong signalId = _gSignalConnectData(webView, "script-dialog", callback, IntPtr.Zero, IntPtr.Zero, 0); _scriptDialogSignalIds[webView] = signalId; return signalId; } public static void DisconnectScriptDialog(IntPtr webView) { if (_scriptDialogSignalIds.TryGetValue(webView, out ulong signalId) && signalId != 0) { g_signal_handler_disconnect(webView, signalId); _scriptDialogSignalIds.Remove(webView); } _scriptDialogCallbacks.Remove(webView); } /// /// Gets the type of a script dialog. /// public static WebKitScriptDialogType GetScriptDialogType(IntPtr dialog) { if (_webkitScriptDialogGetDialogType == null || dialog == IntPtr.Zero) return WebKitScriptDialogType.Alert; return (WebKitScriptDialogType)_webkitScriptDialogGetDialogType(dialog); } /// /// Gets the message from a script dialog. /// public static string? GetScriptDialogMessage(IntPtr dialog) { if (_webkitScriptDialogGetMessage == null || dialog == IntPtr.Zero) return null; IntPtr msgPtr = _webkitScriptDialogGetMessage(dialog); return msgPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(msgPtr); } /// /// Sets the confirmed state for a confirm dialog. /// public static void SetScriptDialogConfirmed(IntPtr dialog, bool confirmed) { _webkitScriptDialogConfirmSetConfirmed?.Invoke(dialog, confirmed); } /// /// Gets the default text for a prompt dialog. /// public static string? GetScriptDialogPromptDefaultText(IntPtr dialog) { if (_webkitScriptDialogPromptGetDefaultText == null || dialog == IntPtr.Zero) return null; IntPtr textPtr = _webkitScriptDialogPromptGetDefaultText(dialog); return textPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(textPtr); } /// /// Sets the text response for a prompt dialog. /// public static void SetScriptDialogPromptText(IntPtr dialog, string text) { _webkitScriptDialogPromptSetText?.Invoke(dialog, text); } /// /// Cleans up native library handles. Call on application shutdown. /// public static void Cleanup() { _loadChangedCallbacks.Clear(); _scriptDialogCallbacks.Clear(); _loadChangedSignalIds.Clear(); _scriptDialogSignalIds.Clear(); if (_gobjectHandle != IntPtr.Zero) { dlclose(_gobjectHandle); _gobjectHandle = IntPtr.Zero; } if (_handle != IntPtr.Zero) { dlclose(_handle); _handle = IntPtr.Zero; } _initialized = false; } }