Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ namespace Avalonia.Controls.Android;
internal class AndroidWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect,
IWebViewAdapterWithCookieManager, IAndroidWebViewPlatformHandle, IWebViewWithPrintWithOptions
{
private const string PostAvWebViewMessageName = "postAvWebViewMessage";
private static bool s_canSetDataDirectorySuffix = true;
private readonly JavaScriptInterface _jsInterface;
private WebView? _webView;
Expand Down Expand Up @@ -93,7 +92,7 @@ public AndroidWebViewAdapter(global::Android.Content.Context parentContext, Andr
_webView.Settings.LoadWithOverviewMode = true;
_webView.Settings.UseWideViewPort = true;
}
_webView.AddJavascriptInterface(_jsInterface, PostAvWebViewMessageName);
_webView.AddJavascriptInterface(_jsInterface, WebViewHelper.PostAvWebViewMessageName);
_webView.SetWebViewClient(new AvaloniaWebViewClient(this));
_webView.SetWebChromeClient(new WebChromeClient());

Expand Down Expand Up @@ -661,14 +660,7 @@ public override void OnPageFinished(WebView? view, string? url)
return;

adapter._webView.EvaluateJavascript(
"""
function invokeCSharpAction(data)
{
var message = typeof data === 'object' ? JSON.stringify(data) : data;
postAvWebViewMessage.postMessage(message);
}
"""
, null);
WebViewHelper.BuildInvokeCSharpActionScript(WebViewHelper.PostAvWebViewMessageName, stringify: true), null);

if (!_lastNavigationCompleted)
{
Expand Down
14 changes: 3 additions & 11 deletions src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ namespace Avalonia.Controls.Gtk;

internal abstract class GtkWebViewAdapter : IWebViewAdapterWithFocus, IGtkWebViewPlatformHandle, IWebViewWithPrintWithOptions
{
private const string PostAvWebViewMessageName = "postAvWebViewMessage";

internal enum WebKitLoadEvent
{
Started,
Expand Down Expand Up @@ -111,17 +109,11 @@ protected GtkWebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs args)
_webViewHandle = webkit_web_view_new_with_context(context);

var contentManager = webkit_web_view_get_user_content_manager(WebViewHandle);
_scriptMessageReceivedSignal = new GtkSignal(contentManager, $"script-message-received::{PostAvWebViewMessageName}", s_scriptMessageReceivedCallback, this);
webkit_user_content_manager_register_script_message_handler(contentManager, PostAvWebViewMessageName);
_scriptMessageReceivedSignal = new GtkSignal(contentManager, $"script-message-received::{WebViewHelper.PostAvWebViewMessageName}", s_scriptMessageReceivedCallback, this);
webkit_user_content_manager_register_script_message_handler(contentManager, WebViewHelper.PostAvWebViewMessageName);

var script = webkit_user_script_new(
$$"""
function invokeCSharpAction(data)
{
var message = typeof data === 'object' ? JSON.stringify(data) : data;
window.webkit.messageHandlers.{{PostAvWebViewMessageName}}.postMessage(message);
}
""",
WebViewHelper.BuildWebKitInvokeCSharpActionScript(),
0, 0, IntPtr.Zero, IntPtr.Zero);
webkit_user_content_manager_add_script(contentManager, script);

Expand Down
16 changes: 16 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Linux/Interop/WpeInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,27 @@ public static partial void webkit_web_view_set_background_color(
[LibraryImport(LibWpeWebKit)]
public static partial IntPtr webkit_web_view_get_user_content_manager(IntPtr webView);

[LibraryImport(LibWpeWebKit)]
public static partial IntPtr webkit_user_content_manager_get_type();

[LibraryImport(LibWpeWebKit)]
public static partial IntPtr webkit_user_content_manager_new();

[LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool webkit_user_content_manager_register_script_message_handler(
IntPtr manager, string name, string? worldName);

[LibraryImport(LibWpeWebKit)]
public static partial void webkit_user_content_manager_add_script(IntPtr manager, IntPtr userScript);

[LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)]
public static partial IntPtr webkit_user_script_new(
string source, int injectedFrames, int injectionTime, IntPtr allowList, IntPtr blockList);

[LibraryImport(LibWpeWebKit)]
public static partial void webkit_user_script_unref(IntPtr userScript);

[LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)]
public static partial void webkit_settings_set_user_agent(IntPtr settings, string? userAgent);

Expand Down
44 changes: 37 additions & 7 deletions src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal sealed unsafe class WpeWebViewAdapter
private IntPtr _exportable;
private IntPtr _viewBackend;
private IntPtr _networkSession;
private IntPtr _userContentManager;
private IntPtr _cookieManager;
private bool _exportableOwnedByWebKit; // true when webkit_web_view_backend_new took ownership
private PixelSize _currentSize;
Expand Down Expand Up @@ -231,15 +232,32 @@ private void Initialize(LinuxWpeWebViewEnvironmentRequestedEventArgs args, Contr
else
_networkSession = WpeInterop.webkit_network_session_get_default();

_userContentManager = WpeInterop.webkit_user_content_manager_new();
if (_userContentManager == IntPtr.Zero)
throw new InvalidOperationException("webkit_user_content_manager_new failed.");

var webViewType = WpeInterop.webkit_web_view_get_type();
var wkBackendType = WpeInterop.webkit_web_view_backend_get_type();
var networkSessionType = WpeInterop.webkit_network_session_get_type();
var keys = new[] { "backend", "network-session" };
var values = new[] { new GValue(wkBackendType, wkBackend), new GValue(networkSessionType, _networkSession) };
_webView = WpeInterop.g_object_new_with_properties(webViewType, 2, keys, values);
var contentManagerType = WpeInterop.webkit_user_content_manager_get_type();
var keys = new[] { "backend", "network-session", "user-content-manager" };
var values = new[]
{
new GValue(wkBackendType, wkBackend),
new GValue(networkSessionType, _networkSession),
new GValue(contentManagerType, _userContentManager)
};
_webView = WpeInterop.g_object_new_with_properties(webViewType, 3, keys, values);
if (_webView == IntPtr.Zero)
throw new InvalidOperationException("webkit_web_view_new failed.");

var viewContentManager = WpeInterop.webkit_web_view_get_user_content_manager(_webView);
if (viewContentManager != IntPtr.Zero && viewContentManager != _userContentManager)
{
WpeInterop.g_object_unref(_userContentManager);
_userContentManager = WpeInterop.g_object_ref(viewContentManager);
}

// 5. Start GLib pump (WebKit needs it for internal IPC)
WpeGLibIntegration.Start();

Expand All @@ -258,11 +276,17 @@ private void Initialize(LinuxWpeWebViewEnvironmentRequestedEventArgs args, Contr
ConnectSignal(_webView, "decide-policy", Marshal.GetFunctionPointerForDelegate(_decidePolicyCallback), selfPtr);
ConnectSignal(_webView, "create", Marshal.GetFunctionPointerForDelegate(_createCallback), selfPtr);

// 8. Register invokeCSharpAction message handler
var contentManager = WpeInterop.webkit_web_view_get_user_content_manager(_webView);
WpeInterop.webkit_user_content_manager_register_script_message_handler(contentManager, "invokeCSharpAction", null);
ConnectSignal(contentManager, "script-message-received::invokeCSharpAction",
// 8. Register the message handler, connecting before registering so that no message can arrive unhandled.
ConnectSignal(_userContentManager, $"script-message-received::{WebViewHelper.PostAvWebViewMessageName}",
Marshal.GetFunctionPointerForDelegate(_scriptMessageCallback), selfPtr);
WpeInterop.webkit_user_content_manager_register_script_message_handler(
_userContentManager, WebViewHelper.PostAvWebViewMessageName, null);

// Inject the invokeCSharpAction wrapper into all frames at document start.
var bridgeScript = WpeInterop.webkit_user_script_new(
WebViewHelper.BuildWebKitInvokeCSharpActionScript(), 0, 0, IntPtr.Zero, IntPtr.Zero);
WpeInterop.webkit_user_content_manager_add_script(_userContentManager, bridgeScript);
WpeInterop.webkit_user_script_unref(bridgeScript);

// 9. Apply settings
var settings = WpeInterop.webkit_web_view_get_settings(_webView);
Expand Down Expand Up @@ -1019,6 +1043,12 @@ public void Dispose()
_webView = IntPtr.Zero;
}

if (_userContentManager != IntPtr.Zero)
{
WpeInterop.g_object_unref(_userContentManager);
_userContentManager = IntPtr.Zero;
}

if (!_exportableOwnedByWebKit && _exportable != IntPtr.Zero)
{
WpeInterop.wpe_view_backend_exportable_fdo_destroy(_exportable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ namespace Avalonia.Controls.Macios;
internal class MaciosWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect,
IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewWithPrint, IAppleWKWebViewPlatformHandle
{
private const string DefaultPostAvWebViewMessageName = "postAvWebViewMessage";

private readonly string _scriptHandlerMessageName;
private readonly NSString _scriptHandlerMessageNameNative;
private readonly WKWebViewConfiguration _config;
Expand All @@ -40,7 +38,7 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options)
_scriptHandler = new WKScriptMessageHandler();
_scriptHandler.DidReceiveScriptMessage += OnScriptHandlerOnDidReceiveScriptMessage;

_scriptHandlerMessageName = options.ScriptHandlerMessageName ?? DefaultPostAvWebViewMessageName;
_scriptHandlerMessageName = options.ScriptHandlerMessageName ?? WebViewHelper.PostAvWebViewMessageName;
_scriptHandlerMessageNameNative = NSString.Create(_scriptHandlerMessageName);
_config = new WKWebViewConfiguration { JavaScriptEnabled = true };
_config.AddScriptMessageHandler(_scriptHandler, _scriptHandlerMessageNameNative);
Expand Down Expand Up @@ -297,7 +295,7 @@ private void OnDelegateOnDecidePolicyNavigation(object? _, WKNavigationDelegate.

private async void OnDelegateOnDidFinishNavigation(object? sender, EventArgs args)
{
_ = await InvokeScript($"function invokeCSharpAction(data){{window.webkit.messageHandlers.{_scriptHandlerMessageName}.postMessage(data);}}");
_ = await InvokeScript(WebViewHelper.BuildWebKitInvokeCSharpActionScript(_scriptHandlerMessageName, stringify: true));

using var url = _webView.Url;
NavigationCompleted?.Invoke(this, new WebViewNavigationCompletedEventArgs { Request = Uri.TryCreate(url!.AbsoluteString, UriKind.Absolute, out var uri) ? uri : null, IsSuccess = true });
Expand Down
25 changes: 25 additions & 0 deletions src/Avalonia.Controls.WebView.Core/WebViewHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,33 @@ namespace Avalonia.Controls;

internal static class WebViewHelper
{
internal const string PostAvWebViewMessageName = "postAvWebViewMessage";

public static Uri EmptyPage { get; } = new("about:blank");

internal static string BuildWebKitInvokeCSharpActionScript(
string messageName = PostAvWebViewMessageName, bool stringify = true) =>
BuildInvokeCSharpActionScript("window.webkit.messageHandlers." + messageName, stringify: stringify);

/// <param name="postObject">Target object to send message to.</param>
/// <param name="postMethod">Method on the <see cref="postObject"/> that should be invoked to pass the message.</param>
/// <param name="stringify">
/// Defines if post data should be JSON serialized,
/// some backends do that automatically when marshall objects to the C# handlers.
/// </param>
internal static string BuildInvokeCSharpActionScript(string postObject,
string postMethod = "postMessage", bool stringify = true)
{
return stringify ?
"function invokeCSharpAction(data){" +
"var message = typeof data === 'object' ? JSON.stringify(data) : data;" +
$"{postObject}.{postMethod}(message);" +
"}" :
"function invokeCSharpAction(data){" +
$"{postObject}.{postMethod}(data);" +
"}";
}

internal static bool IsAnchorNavigation(Uri? currentUrl, Uri? newUrl)
{
if (currentUrl is null || newUrl is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,8 @@ async void InitScript()
{
try
{
var initScript =
"""
window.invokeCSharpAction = function(data) {
var message = typeof data === 'object' ? JSON.stringify(data) : data;
window.external.notify(message);
};
""";
await adapter.InvokeScript(initScript);
await adapter.InvokeScript(WebViewHelper.BuildInvokeCSharpActionScript(
"window.external", postMethod: "notify", stringify: true));
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ public async Task InitializeAsync(WindowsWebView2EnvironmentRequestedEventArgs e
var addScriptCompletion = new AddScriptToExecuteOnDocumentCreatedCompletedHandler();
var webView = TryGetWebView2() ?? throw new InvalidOperationException("WebView2 is not initialized.");
webView.AddScriptToExecuteOnDocumentCreated(
"function invokeCSharpAction(data){window.chrome.webview.postMessage(data);}", addScriptCompletion);
WebViewHelper.BuildInvokeCSharpActionScript("window.chrome.webview", stringify: false), addScriptCompletion);
_ = await addScriptCompletion.Result.Task;

controller.SetIsVisible(1);
Expand Down
Loading