diff --git a/src/Controls/src/Core/Controls.Core.csproj b/src/Controls/src/Core/Controls.Core.csproj index 9e846ac2f5c9..b36c999f9d9d 100644 --- a/src/Controls/src/Core/Controls.Core.csproj +++ b/src/Controls/src/Core/Controls.Core.csproj @@ -40,6 +40,10 @@ + + + + diff --git a/src/Controls/src/Core/HybridWebView/HybridWebView.cs b/src/Controls/src/Core/HybridWebView/HybridWebView.cs index 757d8b6dc665..524c08f7fa2a 100644 --- a/src/Controls/src/Core/HybridWebView/HybridWebView.cs +++ b/src/Controls/src/Core/HybridWebView/HybridWebView.cs @@ -66,6 +66,26 @@ void IHybridWebView.RawMessageReceived(string rawMessage) /// public event EventHandler? RawMessageReceived; + bool IHybridWebView.WebResourceRequested(WebResourceRequestedEventArgs args) + { + var platformArgs = new PlatformHybridWebViewWebResourceRequestedEventArgs(args); + var e = new HybridWebViewWebResourceRequestedEventArgs(platformArgs); + WebResourceRequested?.Invoke(this, e); + return e.Handled; + } + + /// + /// Raised when a web resource is requested. This event allows the application to intercept the request and provide a + /// custom response. + /// The event handler can set the property to true + /// to indicate that the request has been handled and no further processing is needed. If the event handler does set this + /// property to true, it must also call the + /// + /// or + /// method to provide a response to the request. + /// + public event EventHandler? WebResourceRequested; + /// /// Sends a raw message to the code running in the web view. Raw messages have no additional processing. /// diff --git a/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs b/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs new file mode 100644 index 000000000000..9593caac770d --- /dev/null +++ b/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Maui.Controls; + +/// +/// Event arguments for the event. +/// +public class HybridWebViewWebResourceRequestedEventArgs +{ + IReadOnlyDictionary? _headers; + IReadOnlyDictionary? _queryParams; + + internal HybridWebViewWebResourceRequestedEventArgs(PlatformHybridWebViewWebResourceRequestedEventArgs platformArgs) + { + PlatformArgs = platformArgs; + Uri = platformArgs.GetRequestUri() is string uri ? new Uri(uri) : throw new InvalidOperationException("Platform web request did not have a request URI."); + Method = platformArgs.GetRequestMethod() ?? throw new InvalidOperationException("Platform web request did not have a request METHOD."); + } + + /// + /// Initializes a new instance of the class + /// with the specified URI and method. + /// + public HybridWebViewWebResourceRequestedEventArgs(Uri uri, string method) + { + Uri = uri; + Method = method; + } + + /// + /// Gets the platform-specific event arguments. + /// + public PlatformHybridWebViewWebResourceRequestedEventArgs? PlatformArgs { get; } + + /// + /// Gets the URI of the requested resource. + /// + public Uri Uri { get; } + + /// + /// Gets the HTTP method used for the request (e.g., GET, POST). + /// + public string Method { get; } + + /// + /// Gets the headers associated with the request. + /// + public IReadOnlyDictionary Headers => + _headers ??= PlatformArgs?.GetRequestHeaders() ?? new Dictionary(); + + /// + /// Gets the query parameters from the URI. + /// + public IReadOnlyDictionary QueryParameters => + _queryParams ??= WebUtils.ParseQueryString(Uri, false) ?? new Dictionary(); + + /// + /// Gets or sets a value indicating whether the request has been handled. + /// + /// If set to true, the web view will not process the request further and a response + /// must be provided using the + /// + /// or method. + /// If set to false, the web view will continue processing the request as normal. + /// + public bool Handled { get; set; } + + /// + /// Sets the response for the web resource request. + /// + /// This method must be called if the property is set to true. + /// + /// The HTTP status code for the response. + /// The reason phrase for the response. + /// The headers to include in the response. + /// The content of the response as a stream. + public void SetResponse(int code, string reason, IReadOnlyDictionary? headers, Stream? content) + { + _ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid."); + +#if WINDOWS + + // create the response + PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse( + content?.AsRandomAccessStream(), + code, + reason, + PlatformHeaders(headers)); + +#elif IOS || MACCATALYST + + // iOS and MacCatalyst will just wait until DidFinish is called + var task = PlatformArgs.UrlSchemeTask; + + // create and send the response headers + task.DidReceiveResponse(new Foundation.NSHttpUrlResponse( + PlatformArgs.Request.Url, + code, + "HTTP/1.1", + PlatformHeaders(headers))); + + // send the data + if (content is not null && Foundation.NSData.FromStream(content) is { } nsdata) + { + task.DidReceiveData(nsdata); + } + + // let the webview know + task.DidFinish(); + +#elif ANDROID + + // Android requires that we return immediately, even if the data is coming later + + // create and send the response headers + var platformHeaders = PlatformHeaders(headers, out var contentType); + PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse( + contentType, + "UTF-8", + code, + reason, + platformHeaders, + content); + +#endif + } + + /// + /// Sets the asynchronous response for the web resource request. + /// + /// This method must be called if the property is set to true. + /// + /// The HTTP status code for the response. + /// The reason phrase for the response. + /// The headers to include in the response. + /// A task that represents the asynchronous operation of getting the response content. + /// + /// This method is not asynchronous and will return immediately. The actual response will be sent when the content task completes. + /// + public void SetResponse(int code, string reason, IReadOnlyDictionary? headers, Task contentTask) => + SetResponseAsync(code, reason, headers, contentTask).FireAndForget(); + +#pragma warning disable CS1998 // Android implememntation does not use async/await + async Task SetResponseAsync(int code, string reason, IReadOnlyDictionary? headers, Task contentTask) +#pragma warning restore CS1998 + { + _ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid."); + +#if WINDOWS + + // Windows uses a deferral to let the webview know that we are going to be async + using var deferral = PlatformArgs.RequestEventArgs.GetDeferral(); + + // get the actual content + var data = await contentTask; + + // create the response + PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse( + data?.AsRandomAccessStream(), + code, + reason, + PlatformHeaders(headers)); + + // let the webview know + deferral.Complete(); + +#elif IOS || MACCATALYST + + // iOS and MacCatalyst will just wait until DidFinish is called + var task = PlatformArgs.UrlSchemeTask; + + // create and send the response headers + task.DidReceiveResponse(new Foundation.NSHttpUrlResponse( + PlatformArgs.Request.Url, + code, + "HTTP/1.1", + PlatformHeaders(headers))); + + // get the actual content + var data = await contentTask; + + // send the data + if (data is not null && Foundation.NSData.FromStream(data) is { } nsdata) + { + task.DidReceiveData(nsdata); + } + + // let the webview know + task.DidFinish(); + +#elif ANDROID + + // Android requires that we return immediately, even if the data is coming later + + // get the actual content + var stream = new AsyncStream(contentTask, null); + + // create and send the response headers + var platformHeaders = PlatformHeaders(headers, out var contentType); + PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse( + contentType, + "UTF-8", + code, + reason, + platformHeaders, + stream); + +#endif + } + +#if WINDOWS + static string? PlatformHeaders(IReadOnlyDictionary? headers) + { + if (headers?.Count > 0) + { + var sb = new StringBuilder(); + foreach (var header in headers) + { + sb.AppendLine($"{header.Key}: {header.Value}"); + } + return sb.ToString(); + } + return null; + } +#elif IOS || MACCATALYST + static Foundation.NSMutableDictionary? PlatformHeaders(IReadOnlyDictionary? headers) + { + if (headers?.Count > 0) + { + var dic = new Foundation.NSMutableDictionary(); + foreach (var header in headers) + { + dic.Add((Foundation.NSString)header.Key, (Foundation.NSString)header.Value); + } + return dic; + } + return null; + } +#elif ANDROID + static global::Android.Runtime.JavaDictionary? PlatformHeaders(IReadOnlyDictionary? headers, out string contentType) + { + contentType = "application/octet-stream"; + if (headers?.Count > 0) + { + var dic = new global::Android.Runtime.JavaDictionary(); + foreach (var header in headers) + { + if ("Content-Type".Equals(header.Key, StringComparison.OrdinalIgnoreCase)) + { + contentType = header.Value; + } + + dic.Add(header.Key, header.Value); + } + return dic; + } + return null; + } +#endif +} + +/// +/// Extension methods for the class. +/// +public static class HybridWebViewWebResourceRequestedEventArgsExtensions +{ + /// + /// Sets the response for the web resource request with a status code and reason. + /// + /// The event arguments. + /// The HTTP status code for the response. + /// The reason phrase for the response. + public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason) => + e.SetResponse(code, reason, null, (Stream?)null); + + /// + /// Sets the response for the web resource request with a status code, reason, and content type. + /// + /// The event arguments. + /// The HTTP status code for the response. + /// The reason phrase for the response. + /// The content type of the response. + /// The content of the response as a stream. + public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Stream? content) => + e.SetResponse(code, reason, new Dictionary { ["Content-Type"] = contentType }, content); + + /// + /// Sets the response for the web resource request with a status code, reason, and content type. + /// + /// The event arguments. + /// The HTTP status code for the response. + /// The reason phrase for the response. + /// The content type of the response. + /// A task that represents the asynchronous operation of getting the response content. + public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Task contentTask) => + e.SetResponse(code, reason, new Dictionary { ["Content-Type"] = contentType }, contentTask); +} diff --git a/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs b/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs new file mode 100644 index 000000000000..a97c8bec64a4 --- /dev/null +++ b/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui.Controls; + +/// +/// Provides platform-specific information about the event. +/// +public class PlatformHybridWebViewWebResourceRequestedEventArgs +{ +#if WINDOWS + internal PlatformHybridWebViewWebResourceRequestedEventArgs( + global::Microsoft.Web.WebView2.Core.CoreWebView2 sender, + global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs eventArgs) + { + Sender = sender; + RequestEventArgs = eventArgs; + } + + internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequestedEventArgs args) + : this(args.Sender, args.RequestEventArgs) + { + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on Windows. + /// + public global::Microsoft.Web.WebView2.Core.CoreWebView2 Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on Windows. + /// + public global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs RequestEventArgs { get; } + + /// + /// Gets the native request attached to the event. + /// + /// + /// This is only available on Windows. + /// This is equivalent to RequestEventArgs.Request. + /// + public global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequest Request => RequestEventArgs.Request; + + internal string? GetRequestUri() => Request.Uri; + + internal string? GetRequestMethod() => Request.Method; + + internal IReadOnlyDictionary? GetRequestHeaders() => new WrappedHeadersDictionary(Request.Headers); + + class WrappedHeadersDictionary : IReadOnlyDictionary + { + private global::Microsoft.Web.WebView2.Core.CoreWebView2HttpRequestHeaders _headers; + + public WrappedHeadersDictionary(global::Microsoft.Web.WebView2.Core.CoreWebView2HttpRequestHeaders headers) + { + _headers = headers; + } + + public string this[string key] => + _headers.Contains(key) + ? _headers.GetHeader(key) + : throw new KeyNotFoundException($"The key '{key}' was not found."); + + public IEnumerable Keys => + _headers.Select(header => header.Key); + + public IEnumerable Values => + _headers.Select(header => header.Value); + + public int Count => _headers.Count(); + + public bool ContainsKey(string key) => _headers.Contains(key); + + public bool TryGetValue(string key, out string value) + { + if (_headers.Contains(key)) + { + value = _headers.GetHeader(key); + return true; + } + value = string.Empty; + return false; + } + + public IEnumerator> GetEnumerator() => _headers.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + +#elif IOS || MACCATALYST + + internal PlatformHybridWebViewWebResourceRequestedEventArgs( + global::WebKit.WKWebView sender, + global::WebKit.IWKUrlSchemeTask urlSchemeTask) + { + Sender = sender; + UrlSchemeTask = urlSchemeTask; + } + + internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequestedEventArgs args) + : this(args.Sender, args.UrlSchemeTask) + { + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on iOS and Mac Catalyst. + /// + public global::WebKit.WKWebView Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on iOS and Mac Catalyst. + /// + public global::WebKit.IWKUrlSchemeTask UrlSchemeTask { get; } + + /// + /// Gets the native request attached to the event. + /// + /// + /// This is only available on iOS and Mac Catalyst. + /// This is equivalent to UrlSchemeTask.Request. + /// + public Foundation.NSUrlRequest Request => UrlSchemeTask.Request; + + internal string? GetRequestUri() => Request.Url?.AbsoluteString; + + internal string? GetRequestMethod() => Request.HttpMethod; + + internal IReadOnlyDictionary? GetRequestHeaders() => new WrappedHeadersDictionary(Request.Headers); + + class WrappedHeadersDictionary : IReadOnlyDictionary + { + Foundation.NSDictionary _headers; + + public WrappedHeadersDictionary(Foundation.NSDictionary headers) + { + _headers = headers; + } + + public string this[string key] => + TryGetValue(key, out var value) + ? value + : throw new KeyNotFoundException($"The key '{key}' was not found."); + + public IEnumerable Keys => _headers.Keys.Select(k => k.ToString()); + + public IEnumerable Values => _headers.Values.Select(v => v.ToString()); + + public int Count => (int)_headers.Count; + + public bool ContainsKey(string key) + { + using var nskey = new Foundation.NSString(key); + return _headers.ContainsKey(nskey); + } + + public bool TryGetValue(string key, out string value) + { + using var nsKey = new Foundation.NSString(key); + if (_headers.ContainsKey(nsKey)) + { + value = _headers[nsKey].ToString(); + return true; + } + value = string.Empty; + return false; + } + + public IEnumerator> GetEnumerator() + { + foreach (var pair in _headers) + { + yield return new KeyValuePair(pair.Key.ToString(), pair.Value.ToString()); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + +#elif ANDROID + + Action _setResponse; + private global::Android.Webkit.WebResourceResponse? _response; + + internal PlatformHybridWebViewWebResourceRequestedEventArgs( + global::Android.Webkit.WebView sender, + global::Android.Webkit.IWebResourceRequest request, + Action setResponse) + { + Sender = sender; + Request = request; + _setResponse = setResponse; + } + + internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequestedEventArgs args) + : this(args.Sender, args.Request, (response) => args.Response = response) + { + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on Android. + /// + public global::Android.Webkit.WebView Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on Android. + /// + public global::Android.Webkit.IWebResourceRequest Request { get; } + + /// + /// Gets or sets the native response to return to the web view. + /// + /// This property must be set to a valid response if the property is set to true. + /// + /// This is only available on Android. + /// + public global::Android.Webkit.WebResourceResponse? Response + { + get => _response; + set + { + _response = value; + _setResponse(value); + } + } + + internal string? GetRequestUri() => Request.Url?.ToString(); + + internal string? GetRequestMethod() => Request.Method; + + internal IReadOnlyDictionary? GetRequestHeaders() => Request.RequestHeaders?.AsReadOnly(); + +#else + + internal PlatformHybridWebViewWebResourceRequestedEventArgs() + { + } + + internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequestedEventArgs args) + { + } + +#pragma warning disable CA1822 // Mark members as static + internal string? GetRequestUri() => null; + + internal string? GetRequestMethod() => null; + + internal IReadOnlyDictionary? GetRequestHeaders() => null; +#pragma warning restore CA1822 // Mark members as static + +#endif +} diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index 14eb191c737e..3e2e877effd5 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -23,12 +23,33 @@ Microsoft.Maui.Controls.DatePicker.MaximumDate.get -> System.DateTime? Microsoft.Maui.Controls.DatePicker.MinimumDate.get -> System.DateTime? Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.ILineHeightElement Microsoft.Maui.Controls.ILineHeightElement.LineHeight.get -> double Microsoft.Maui.Controls.ILineHeightElement.OnLineHeightChanged(double oldValue, double newValue) -> void Microsoft.Maui.Controls.Internals.TextTransformUtilities +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Request.get -> Android.Webkit.IWebResourceRequest! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Response.get -> Android.Webkit.WebResourceResponse? +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Response.set -> void +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Sender.get -> Android.Webkit.WebView! +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void Microsoft.Maui.Controls.ITextAlignmentElement Microsoft.Maui.Controls.ITextAlignmentElement.HorizontalTextAlignment.get -> Microsoft.Maui.TextAlignment Microsoft.Maui.Controls.ITextAlignmentElement.OnHorizontalTextAlignmentPropertyChanged(Microsoft.Maui.TextAlignment oldValue, Microsoft.Maui.TextAlignment newValue) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index d4c3b90b06ef..69a10678e455 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,4 +1,21 @@ #nullable enable +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Request.get -> Foundation.NSUrlRequest! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! Microsoft.Maui.Controls.ShadowTypeConverter Microsoft.Maui.Controls.ShadowTypeConverter.ShadowTypeConverter() -> void *REMOVED*Microsoft.Maui.Controls.DatePicker.Date.get -> System.DateTime @@ -22,6 +39,9 @@ override Microsoft.Maui.Controls.ShadowTypeConverter.CanConvertFrom(System.Compo override Microsoft.Maui.Controls.ShadowTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Controls.ShadowTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object! override Microsoft.Maui.Controls.ShadowTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type? destinationType) -> object! +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void ~static Microsoft.Maui.Controls.Internals.TextTransformUtilities.GetTransformedText(string source, Microsoft.Maui.TextTransform textTransform) -> string ~static Microsoft.Maui.Controls.Internals.TextTransformUtilities.SetPlainText(Microsoft.Maui.Controls.InputView inputView, string platformText) -> void ~Microsoft.Maui.Controls.Page.DisplayActionSheetAsync(string title, string cancel, string destruction, Microsoft.Maui.FlowDirection flowDirection, params string[] buttons) -> System.Threading.Tasks.Task diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index dd76e3f7ccd7..2d9d4dbaee19 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,4 +1,21 @@ #nullable enable +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Request.get -> Foundation.NSUrlRequest! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! Microsoft.Maui.Controls.ShadowTypeConverter Microsoft.Maui.Controls.ShadowTypeConverter.ShadowTypeConverter() -> void *REMOVED*Microsoft.Maui.Controls.DatePicker.Date.get -> System.DateTime @@ -22,6 +39,9 @@ override Microsoft.Maui.Controls.ShadowTypeConverter.CanConvertFrom(System.Compo override Microsoft.Maui.Controls.ShadowTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Controls.ShadowTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object! override Microsoft.Maui.Controls.ShadowTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type? destinationType) -> object! +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void ~static Microsoft.Maui.Controls.Internals.TextTransformUtilities.GetTransformedText(string source, Microsoft.Maui.TextTransform textTransform) -> string ~static Microsoft.Maui.Controls.Internals.TextTransformUtilities.SetPlainText(Microsoft.Maui.Controls.InputView inputView, string platformText) -> void ~Microsoft.Maui.Controls.Page.DisplayActionSheetAsync(string title, string cancel, string destruction, Microsoft.Maui.FlowDirection flowDirection, params string[] buttons) -> System.Threading.Tasks.Task diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index d42197da3be4..fee6edfd1fe9 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -23,6 +23,19 @@ Microsoft.Maui.Controls.DatePicker.MaximumDate.get -> System.DateTime? Microsoft.Maui.Controls.DatePicker.MinimumDate.get -> System.DateTime? Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.ILineHeightElement @@ -45,6 +58,13 @@ Microsoft.Maui.Controls.TimeChangedEventArgs.TimeChangedEventArgs(System.TimeSpa ~Microsoft.Maui.Controls.ITextElement.OnTextColorPropertyChanged(Microsoft.Maui.Graphics.Color oldValue, Microsoft.Maui.Graphics.Color newValue) -> void ~Microsoft.Maui.Controls.ITextElement.TextColor.get -> Microsoft.Maui.Graphics.Color ~Microsoft.Maui.Controls.ITextElement.UpdateFormsText(string original, Microsoft.Maui.TextTransform transform) -> string +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Request.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequest! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.RequestEventArgs.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs! +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void override Microsoft.Maui.Controls.Handlers.Items.SelectableItemsViewHandler.UpdateItemsLayout() -> void Microsoft.Maui.Controls.ShadowTypeConverter Microsoft.Maui.Controls.ShadowTypeConverter.ShadowTypeConverter() -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index b2e48b185fe9..65d705ecfc02 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -23,6 +23,19 @@ Microsoft.Maui.Controls.DatePicker.MaximumDate.get -> System.DateTime? Microsoft.Maui.Controls.DatePicker.MinimumDate.get -> System.DateTime? Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.ILineHeightElement @@ -45,6 +58,10 @@ Microsoft.Maui.Controls.ITextElement.OnCharacterSpacingPropertyChanged(double ol Microsoft.Maui.Controls.ITextElement.OnTextTransformChanged(Microsoft.Maui.TextTransform oldValue, Microsoft.Maui.TextTransform newValue) -> void Microsoft.Maui.Controls.ITextElement.TextTransform.get -> Microsoft.Maui.TextTransform Microsoft.Maui.Controls.ITextElement.TextTransform.set -> void +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void Microsoft.Maui.Controls.ShadowTypeConverter Microsoft.Maui.Controls.ShadowTypeConverter.ShadowTypeConverter() -> void Microsoft.Maui.Controls.StyleableElement.Style.get -> Microsoft.Maui.Controls.Style? diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 04ac604659b5..65413be22d35 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -23,6 +23,19 @@ Microsoft.Maui.Controls.DatePicker.MaximumDate.get -> System.DateTime? Microsoft.Maui.Controls.DatePicker.MinimumDate.get -> System.DateTime? Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.get -> bool +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Handled.set -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Headers.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.HybridWebViewWebResourceRequestedEventArgs(System.Uri! uri, string! method) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Method.get -> string! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs? +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.QueryParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.IO.Stream? content) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.SetResponse(int code, string! reason, System.Collections.Generic.IReadOnlyDictionary? headers, System.Threading.Tasks.Task! contentTask) -> void +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs.Uri.get -> System.Uri! +Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.ILineHeightElement @@ -39,6 +52,7 @@ Microsoft.Maui.Controls.ITextElement.OnCharacterSpacingPropertyChanged(double ol Microsoft.Maui.Controls.ITextElement.OnTextTransformChanged(Microsoft.Maui.TextTransform oldValue, Microsoft.Maui.TextTransform newValue) -> void Microsoft.Maui.Controls.ITextElement.TextTransform.get -> Microsoft.Maui.TextTransform Microsoft.Maui.Controls.ITextElement.TextTransform.set -> void +Microsoft.Maui.Controls.PlatformHybridWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.ShadowTypeConverter Microsoft.Maui.Controls.ShadowTypeConverter.ShadowTypeConverter() -> void Microsoft.Maui.Controls.StyleableElement.Style.get -> Microsoft.Maui.Controls.Style? @@ -58,6 +72,9 @@ override Microsoft.Maui.Controls.ShadowTypeConverter.ConvertTo(System.ComponentM ~Microsoft.Maui.Controls.Page.DisplayAlertAsync(string title, string message, string accept, string cancel, Microsoft.Maui.FlowDirection flowDirection) -> System.Threading.Tasks.Task ~Microsoft.Maui.Controls.Page.DisplayAlertAsync(string title, string message, string cancel) -> System.Threading.Tasks.Task ~Microsoft.Maui.Controls.Page.DisplayAlertAsync(string title, string message, string cancel, Microsoft.Maui.FlowDirection flowDirection) -> System.Threading.Tasks.Task +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.IO.Stream? content) -> void +static Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgsExtensions.SetResponse(this Microsoft.Maui.Controls.HybridWebViewWebResourceRequestedEventArgs! e, int code, string! reason, string! contentType, System.Threading.Tasks.Task! contentTask) -> void static Microsoft.Maui.Controls.ViewExtensions.FadeToAsync(this Microsoft.Maui.Controls.VisualElement! view, double opacity, uint length = 250, Microsoft.Maui.Easing? easing = null) -> System.Threading.Tasks.Task! static Microsoft.Maui.Controls.ViewExtensions.LayoutToAsync(this Microsoft.Maui.Controls.VisualElement! view, Microsoft.Maui.Graphics.Rect bounds, uint length = 250, Microsoft.Maui.Easing? easing = null) -> System.Threading.Tasks.Task! static Microsoft.Maui.Controls.ViewExtensions.RelRotateToAsync(this Microsoft.Maui.Controls.VisualElement! view, double drotation, uint length = 250, Microsoft.Maui.Easing? easing = null) -> System.Threading.Tasks.Task! diff --git a/src/Controls/src/Core/Shell/ShellRouteParameters.cs b/src/Controls/src/Core/Shell/ShellRouteParameters.cs index 495f55b02ac3..6957aac97948 100644 --- a/src/Controls/src/Core/Shell/ShellRouteParameters.cs +++ b/src/Controls/src/Core/Shell/ShellRouteParameters.cs @@ -87,7 +87,7 @@ internal void ResetToQueryParameters() internal void SetQueryStringParameters(string query) { - var queryStringParameters = ParseQueryString(query); + var queryStringParameters = ParseQueryString(query.AsSpan()); if (queryStringParameters == null || queryStringParameters.Count == 0) return; @@ -98,20 +98,14 @@ internal void SetQueryStringParameters(string query) } } - static Dictionary ParseQueryString(string query) + static Dictionary ParseQueryString(ReadOnlySpan query) { - if (query.StartsWith("?", StringComparison.Ordinal)) - query = query.Substring(1); + if (query.Length > 0 && query[0] == '?') + query = query.Slice(1); + Dictionary lookupDict = new(StringComparer.Ordinal); - if (query == null) - return lookupDict; - foreach (var part in query.Split('&')) - { - var p = part.Split('='); - if (p.Length != 2) - continue; - lookupDict[p[0]] = p[1]; - } + + WebUtils.UnpackParameters(query, lookupDict); return lookupDict; } diff --git a/src/Controls/tests/Core.UnitTests/ShellParameterPassingTests.cs b/src/Controls/tests/Core.UnitTests/ShellParameterPassingTests.cs index b19296cead87..8ce673af4b4d 100644 --- a/src/Controls/tests/Core.UnitTests/ShellParameterPassingTests.cs +++ b/src/Controls/tests/Core.UnitTests/ShellParameterPassingTests.cs @@ -34,7 +34,7 @@ public async Task ReNavigatingToCurrentLocationPassesParameters(bool useDataTemp await shell.GoToAsync($"//content?{nameof(ShellTestPage.SomeQueryParameter)}=4321"); Assert.Equal("4321", page.SomeQueryParameter); await shell.GoToAsync($"//content?{nameof(ShellTestPage.SomeQueryParameter)}"); - Assert.Null(page.SomeQueryParameter); + Assert.Empty(page.SomeQueryParameter); } [Fact] diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs index ce78da8d9f97..b9f66da7ba7b 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs @@ -1,10 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Pipelines; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; @@ -471,6 +477,276 @@ public async Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) Assert.NotNull(ex.InnerException.StackTrace); } + [Fact] + public Task RequestsCanBeInterceptedAndCustomDataReturned() => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (e.Uri.Host == "0.0.0.1") + { + // 1. Create the response data + var response = new EchoResponseObject { message = $"Hello real endpoint (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})" }; + var responseData = JsonSerializer.SerializeToUtf8Bytes(response); + var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); + + // 2. Create the response + e.SetResponse(200, "OK", "application/json", new MemoryStream(responseData)); + + // 3. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + "RequestsWithAppUriCanBeIntercepted", + HybridWebViewTestContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); + }); + + [Fact] + public Task RequestsCanBeInterceptedAndAsyncCustomDataReturned() => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (e.Uri.Host == "0.0.0.1") + { + // 1. Create the response + e.SetResponse(200, "OK", "application/json", GetDataAsync(e.QueryParameters)); + + // 2. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + "RequestsWithAppUriCanBeIntercepted", + HybridWebViewTestContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); + + static async Task GetDataAsync(IReadOnlyDictionary queryParams) + { + var response = new EchoResponseObject { message = $"Hello real endpoint (param1={queryParams["param1"]}, param2={queryParams["param2"]})" }; + + var ms = new MemoryStream(); + + await Task.Delay(1000); + await JsonSerializer.SerializeAsync(ms, response); + await Task.Delay(1000); + + ms.Position = 0; + + return ms; + } + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCustomDataReturnedForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + // 1. Get the request from the platform args + var name = e.Headers["X-Echo-Name"]; + + // 2. Create the response data + var response = new EchoResponseObject + { + message = $"Hello {name} (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})", + }; + var responseData = JsonSerializer.SerializeToUtf8Bytes(response); + var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); + + // 3. Create the response + var headers = new Dictionary + { + ["Content-Length"] = responseLength, + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }; + e.SetResponse(200, "OK", headers, new MemoryStream(responseData)); + + // 4. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + HybridWebViewTestContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello Matthew (param1=value1, param2=value2)", responseObject.message); + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndHeadersAddedForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + const string ExpectedHeaderValue = "My Header Value"; + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri)) + { +#if WINDOWS + // Add the desired header for Windows by modifying the request + e.PlatformArgs.Request.Headers.SetHeader("X-Request-Header", ExpectedHeaderValue); +#elif IOS || MACCATALYST + // We are going to handle this ourselves + e.Handled = true; + + // Intercept the request and add the desired header to a copy of the request + var task = e.PlatformArgs.UrlSchemeTask; + + // Create a mutable copy of the request (this preserves all existing headers and properties) + var request = e.PlatformArgs.Request.MutableCopy() as Foundation.NSMutableUrlRequest; + + // Set the URL to the desired request URL as iOS only allows us to intercept non-https requests + request.Url = new("https://echo.free.beeceptor.com/sample-request"); + + // Add our custom header + var headers = request.Headers.MutableCopy() as Foundation.NSMutableDictionary; + headers[(Foundation.NSString)"X-Request-Header"] = (Foundation.NSString)ExpectedHeaderValue; + request.Headers = headers; + + // Create a session configuration and session to send the request + var configuration = Foundation.NSUrlSessionConfiguration.DefaultSessionConfiguration; + var session = Foundation.NSUrlSession.FromConfiguration(configuration); + + // Create a data task to send the request and get the response + var dataTask = session.CreateDataTask(request, (data, response, error) => + { + if (error is not null) + { + // Handle the error by completing the task with an error response + task.DidFailWithError(error); + return; + } + + if (response is Foundation.NSHttpUrlResponse httpResponse) + { + // Forward the response headers and status + task.DidReceiveResponse(httpResponse); + + // Forward the response body if any + if (data != null) + { + task.DidReceiveData(data); + } + + // Complete the task + task.DidFinish(); + } + else + { + // Fallback for non-HTTP responses or unexpected response type + task.DidFailWithError(new Foundation.NSError(new Foundation.NSString("HybridWebViewError"), -1, null)); + } + }); + + // Start the request + dataTask.Resume(); +#elif ANDROID + // We are going to handle this ourselves + e.Handled = true; + + // Intercept the request and add the desired header to a new request + var request = e.PlatformArgs.Request; + + // Copy the request + var url = new Java.Net.URL(request.Url.ToString()); + var connection = (Java.Net.HttpURLConnection)url.OpenConnection(); + connection.RequestMethod = request.Method; + foreach (var header in request.RequestHeaders) + { + connection.SetRequestProperty(header.Key, header.Value); + } + + // Add our custom header + connection.SetRequestProperty("X-Request-Header", ExpectedHeaderValue); + + // Set the response property + e.PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse( + connection.ContentType, + connection.ContentEncoding ?? "UTF-8", + (int)connection.ResponseCode, + connection.ResponseMessage, + new Dictionary + { + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }, + connection.InputStream); +#endif + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + HybridWebViewTestContext.Default.ResponseObject); + + Assert.NotNull(responseObject); + Assert.NotNull(responseObject.headers); + Assert.True(responseObject.headers.TryGetValue("X-Request-Header", out var actualHeaderValue)); + Assert.Equal(ExpectedHeaderValue, actualHeaderValue); + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCancelledForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri)) + { + // 1. Create the response + e.SetResponse(403, "Forbidden"); + + // 2. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + await Assert.ThrowsAsync(() => + hybridWebView.InvokeJavaScriptAsync( + function, + HybridWebViewTestContext.Default.ResponseObject)); + }); + async Task RunExceptionTest(string method, int errorType) { Exception exception = null; @@ -521,7 +797,7 @@ await AttachAndRun(hybridWebView, async handler => await WebViewHelpers.WaitForHybridWebViewLoaded(hybridWebView); // This is randomly failing on iOS, so let's add a timeout to avoid device tests running for hours - await test(hybridWebView).WaitAsync(TimeSpan.FromSeconds(5)); + await test(hybridWebView); }); } @@ -663,8 +939,26 @@ public class ComputationResult public string operationName { get; set; } } + public class ResponseObject + { + public string method { get; set; } + public string protocol { get; set; } + public string host { get; set; } + public string path { get; set; } + public string ip { get; set; } + public Dictionary headers { get; set; } + public Dictionary parsedQueryParams { get; set; } + } + + public class EchoResponseObject + { + public string message { get; set; } + } + [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(ComputationResult))] + [JsonSerializable(typeof(ResponseObject))] + [JsonSerializable(typeof(EchoResponseObject))] [JsonSerializable(typeof(int))] [JsonSerializable(typeof(decimal))] [JsonSerializable(typeof(bool))] @@ -717,5 +1011,88 @@ await Retry(async () => }, createExceptionWithTimeoutMS: (int timeoutInMS) => Task.FromResult(new Exception($"Waited {timeoutInMS}ms but couldn't get status element to have a non-empty value."))); } } + + class AsyncStream : Stream + { + readonly Task _streamTask; + Stream _stream; + bool _isDisposed; + + public AsyncStream(Task streamTask) + { + _streamTask = streamTask ?? throw new ArgumentNullException(nameof(streamTask)); + } + + async Task GetStreamAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_isDisposed, nameof(AsyncStream)); + + if (_stream != null) + return _stream; + + _stream = await _streamTask.ConfigureAwait(false); + return _stream; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var stream = await GetStreamAsync(cancellationToken).ConfigureAwait(false); + return await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var stream = GetStreamAsync().GetAwaiter().GetResult(); + return stream.Read(buffer, offset, count); + } + + public override void Flush() => throw new NotSupportedException(); + + public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override bool CanRead => !_isDisposed; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + _stream?.Dispose(); + + _isDisposed = true; + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + return; + + if (_stream != null) + await _stream.DisposeAsync().ConfigureAwait(false); + + _isDisposed = true; + await base.DisposeAsync().ConfigureAwait(false); + } + } } } diff --git a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html index c024c26fa43c..851a950aeb37 100644 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html +++ b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html @@ -93,6 +93,38 @@ return response.status; } + async function RequestsWithAppUriCanBeIntercepted() { + const response = await fetch(`${window.location.origin}/api/sample?param1=value1¶m2=value2`); + const jsonData = await response.json(); + return jsonData; + } + + async function RequestsCanBeIntercepted() { + const response = await fetch('https://echo.free.beeceptor.com/sample-request?param1=value1¶m2=value2', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Test-Header': 'Test Value', + 'X-Echo-Name': 'Matthew' + } + }); + const jsonData = await response.json(); + return jsonData; + } + + async function RequestsWithCustomSchemeCanBeIntercepted() { + const response = await fetch('app://echoservice/?param1=value1¶m2=value2', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Test-Header': 'Test Value', + 'X-Echo-Name': 'Matthew' + } + }); + const jsonData = await response.json(); + return jsonData; + } + function ThrowAnError(value, errorType) { if (errorType === 1) { // throw number throw value; diff --git a/src/Core/src/Core/IHybridWebView.cs b/src/Core/src/Core/IHybridWebView.cs index 7b2efa5b7923..6713f7b2b01c 100644 --- a/src/Core/src/Core/IHybridWebView.cs +++ b/src/Core/src/Core/IHybridWebView.cs @@ -65,5 +65,16 @@ public interface IHybridWebView : IView JsonTypeInfo returnTypeJsonTypeInfo, object?[]? paramValues = null, JsonTypeInfo?[]? paramJsonTypeInfos = null); + + /// + /// Invoked when a web resource is requested. This event can be used to intercept requests and provide custom responses. + /// + /// The event arguments containing the request details. + /// true if the request was handled; otherwise, false. +#if NETSTANDARD + bool WebResourceRequested(WebResourceRequestedEventArgs args); +#else + bool WebResourceRequested(WebResourceRequestedEventArgs args) => false; +#endif } } diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs index 020e93cf4e61..c5489da7eb95 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; -using System.Text; using System.Threading.Tasks; using System.Web; +using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; @@ -16,7 +15,6 @@ namespace Microsoft.Maui.Handlers public partial class HybridWebViewHandler : ViewHandler { private readonly HybridWebView2Proxy _proxy = new(); - private readonly Lazy _404MessageBuffer = new(() => Encoding.UTF8.GetBytes("Resource not found (404)").AsBuffer()); protected override WebView2 CreatePlatformView() { @@ -104,30 +102,72 @@ private void OnWebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceive private async void OnWebResourceRequested(CoreWebView2 sender, CoreWebView2WebResourceRequestedEventArgs eventArgs) { - // Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method. - using var deferral = eventArgs.GetDeferral(); - - var (stream, contentType, statusCode, reason) = await GetResponseStreamAsync(eventArgs.Request.Uri); - var contentLength = stream?.Size ?? 0; - var headers = - $""" - Content-Type: {contentType} - Content-Length: {contentLength} - """; - - eventArgs.Response = sender.Environment!.CreateWebResourceResponse( - Content: stream, - StatusCode: statusCode, - ReasonPhrase: reason, - Headers: headers); - - // Notify WebView2 that the deferred (async) operation is complete and we set a response. - deferral.Complete(); + var url = eventArgs.Request.Uri; + + var logger = MauiContext?.CreateLogger(); + + logger?.LogDebug("Intercepting request for {Url}.", url); + + // 1. First check if the app wants to modify or override the request. + { + // 1.a. First, create the event args + var platformArgs = new WebResourceRequestedEventArgs(sender, eventArgs); + + // 1.b. Trigger the event for the app + var handled = VirtualView.WebResourceRequested(platformArgs); + + // 1.c. If the app reported that it completed the request, then we do nothing more + if (handled) + { + logger?.LogDebug("Request for {Url} was handled by the user.", url); + + return; + } + } + + // 2. If this is an app request, then assume the request is for a local resource. + if (new Uri(url) is Uri uri && AppOriginUri.IsBaseOf(uri)) + { + logger?.LogDebug("Request for {Url} will be handled by .NET MAUI.", url); + + // 2.a. Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method. + using var deferral = eventArgs.GetDeferral(); + + // 2.b. Check if the request is for a local resource + var (stream, contentType, statusCode, reason) = await GetResponseStreamAsync(eventArgs.Request.Uri, logger); + + // 2.c. Create the response header + var headers = ""; + if (stream?.Size is { } contentLength && contentLength > 0) + { + headers += $"Content-Length: {contentLength}{Environment.NewLine}"; + } + if (contentType is not null) + { + headers += $"Content-Type: {contentType}{Environment.NewLine}"; + } + + // 2.d. If something was found, return the content + eventArgs.Response = sender.Environment!.CreateWebResourceResponse( + Content: stream, + StatusCode: statusCode, + ReasonPhrase: reason, + Headers: headers); + + // 2.e. Notify WebView2 that the deferred (async) operation is complete and we set a response. + deferral.Complete(); + } + + // 3. If the request is not handled by the app nor is it a local source, then we let the WebView2 + // handle the request as it would normally do. This means that it will try to load the resource + // from the internet or from the local cache. + + logger?.LogDebug("Request for {Url} was not handled.", url); } - private async Task<(IRandomAccessStream Stream, string ContentType, int StatusCode, string Reason)> GetResponseStreamAsync(string url) + private async Task<(IRandomAccessStream? Stream, string? ContentType, int StatusCode, string Reason)> GetResponseStreamAsync(string url, ILogger? logger) { - var requestUri = HybridWebViewQueryStringHelper.RemovePossibleQueryString(url); + var requestUri = WebUtils.RemovePossibleQueryString(url); if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri)) { @@ -136,6 +176,8 @@ private async void OnWebResourceRequested(CoreWebView2 sender, CoreWebView2WebRe // 1. Try special InvokeDotNet path if (relativePath == InvokeDotNetPath) { + logger?.LogDebug("Request for {Url} will be handled by the .NET method invoker.", url); + var fullUri = new Uri(url); var invokeQueryString = HttpUtility.ParseQueryString(fullUri.Query); var contentBytes = await InvokeDotNetAsync(invokeQueryString); @@ -158,8 +200,8 @@ private async void OnWebResourceRequested(CoreWebView2 sender, CoreWebView2WebRe { if (!ContentTypeProvider.TryGetContentType(relativePath, out contentType!)) { - // TODO: Log this contentType = "text/plain"; + logger?.LogWarning("Could not determine content type for '{relativePath}'", relativePath); } } @@ -169,14 +211,16 @@ private async void OnWebResourceRequested(CoreWebView2 sender, CoreWebView2WebRe if (contentStream is not null) { // 3.a. If something was found, return the content + logger?.LogDebug("Request for {Url} will return an app package file.", url); + var ras = await CopyContentToRandomAccessStreamAsync(contentStream); return (Stream: ras, ContentType: contentType, StatusCode: 200, Reason: "OK"); } } // 3.b. Otherwise, return a 404 - var ras404 = await CopyContentToRandomAccessStreamAsync(_404MessageBuffer.Value); - return (Stream: ras404, ContentType: "text/plain", StatusCode: 404, Reason: "Not Found"); + logger?.LogDebug("Request for {Url} could not be fulfilled.", url); + return (Stream: null, ContentType: null, StatusCode: 404, Reason: "Not Found"); } static async Task CopyContentToRandomAccessStreamAsync(Stream content) @@ -223,7 +267,7 @@ private async Task TryInitializeWebView2(WebView2 webView) webView.CoreWebView2.Settings.AreDevToolsEnabled = Handler?.DeveloperTools.Enabled ?? false; webView.CoreWebView2.Settings.IsWebMessageEnabled = true; - webView.CoreWebView2.AddWebResourceRequestedFilter($"{AppOrigin}*", CoreWebView2WebResourceContext.All); + webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); webView.WebMessageReceived += OnWebMessageReceived; webView.CoreWebView2.WebResourceRequested += OnWebResourceRequested; diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs index 137f2950e66a..8f33a76b2815 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using System.Runtime.Versioning; -using System.Text; using System.Threading.Tasks; using System.Web; using Foundation; @@ -130,7 +129,6 @@ public void DidReceiveScriptMessage(WKUserContentController userContentControlle private class SchemeHandler : NSObject, IWKUrlSchemeHandler { private readonly WeakReference _webViewHandler; - private readonly Lazy _404MessageBytes = new(() => Encoding.UTF8.GetBytes("Resource not found (404)")); public SchemeHandler(HybridWebViewHandler webViewHandler) { @@ -151,37 +149,83 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche return; } - var url = urlSchemeTask.Request.Url?.AbsoluteString ?? ""; + var url = urlSchemeTask.Request.Url.AbsoluteString; + if (string.IsNullOrEmpty(url)) + { + return; + } + + var logger = Handler.MauiContext?.CreateLogger(); + + logger?.LogDebug("Intercepting request for {Url}.", url); + + // 1. First check if the app wants to modify or override the request. + { + // 1.a. First, create the event args + var platformArgs = new WebResourceRequestedEventArgs(webView, urlSchemeTask); + + // 1.b. Trigger the event for the app + var handled = Handler.VirtualView.WebResourceRequested(platformArgs); + + // 1.c. If the app reported that it completed the request, then we do nothing more + if (handled) + { + logger?.LogDebug("Request for {Url} was handled by the user.", url); - var (bytes, contentType, statusCode) = await GetResponseBytesAsync(url); + return; + } + } - using (var dic = new NSMutableDictionary()) + // 2. If this is an app request, then assume the request is for a local resource. + if (new Uri(url) is Uri uri && AppOriginUri.IsBaseOf(uri)) { - dic.Add((NSString)"Content-Length", (NSString)bytes.Length.ToString(CultureInfo.InvariantCulture)); - dic.Add((NSString)"Content-Type", (NSString)contentType); - // Disable local caching. This will prevent user scripts from executing correctly. - dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store"); + logger?.LogDebug("Request for {Url} will be handled by .NET MAUI.", url); + + // 2.a. Check if the request is for a local resource + var (bytes, contentType, statusCode) = await GetResponseBytesAsync(url, logger); + + // 2.b. Return the response header + using var dic = new NSMutableDictionary(); + if (contentType is not null) + { + dic[(NSString)"Content-Type"] = (NSString)contentType; + } + if (bytes?.Length > 0) + { + // Disable local caching which would otherwise prevent user scripts from executing correctly. + dic[(NSString)"Cache-Control"] = (NSString)"no-cache, max-age=0, must-revalidate, no-store"; + dic[(NSString)"Content-Length"] = (NSString)bytes.Length.ToString(CultureInfo.InvariantCulture); + } + + using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, statusCode, "HTTP/1.1", dic); + urlSchemeTask.DidReceiveResponse(response); - if (urlSchemeTask.Request.Url != null) + // 2.c. Return the body + if (bytes?.Length > 0) { - using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, statusCode, "HTTP/1.1", dic); - urlSchemeTask.DidReceiveResponse(response); + urlSchemeTask.DidReceiveData(NSData.FromArray(bytes)); } + + // 2.d. Finish the task + urlSchemeTask.DidFinish(); } - urlSchemeTask.DidReceiveData(NSData.FromArray(bytes)); - urlSchemeTask.DidFinish(); + // 3. If the request is not handled by the app nor is it a local source, then we let the WKWebView + // handle the request as it would normally do. This means that it will try to load the resource + // from the internet or from the local cache. + + logger?.LogDebug("Request for {Url} was not handled.", url); } - private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytesAsync(string? url) + private async Task<(byte[]? ResponseBytes, string? ContentType, int StatusCode)> GetResponseBytesAsync(string url, ILogger? logger) { if (Handler is null) { - return (_404MessageBytes.Value, ContentType: "text/plain", StatusCode: 404); + return (null, ContentType: null, StatusCode: 404); } var fullUrl = url; - url = HybridWebViewQueryStringHelper.RemovePossibleQueryString(url); + url = WebUtils.RemovePossibleQueryString(url); if (new Uri(url) is Uri uri && AppOriginUri.IsBaseOf(uri)) { @@ -192,6 +236,8 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche // 1. Try special InvokeDotNet path if (relativePath == InvokeDotNetPath) { + logger?.LogDebug("Request for {Url} will be handled by the .NET method invoker.", url); + var fullUri = new Uri(fullUrl!); var invokeQueryString = HttpUtility.ParseQueryString(fullUri.Query); var contentBytes = await Handler.InvokeDotNetAsync(invokeQueryString); @@ -213,8 +259,8 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { if (!ContentTypeProvider.TryGetContentType(relativePath, out contentType!)) { - // TODO: Log this contentType = "text/plain"; + logger?.LogWarning("Could not determine content type for '{relativePath}'", relativePath); } } @@ -222,11 +268,16 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche if (File.Exists(assetPath)) { + // 2.a. If something was found, return the content + logger?.LogDebug("Request for {Url} will return an app package file.", url); + return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200); } } - return (_404MessageBytes.Value, ContentType: "text/plain", StatusCode: 404); + // 2.b. Otherwise, return a 404 + logger?.LogDebug("Request for {Url} could not be fulfilled.", url); + return (null, ContentType: null, StatusCode: 404); } [Export("webView:stopURLSchemeTask:")] diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewQueryStringHelper.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewQueryStringHelper.cs deleted file mode 100644 index a61918675980..000000000000 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewQueryStringHelper.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -#if !NETSTANDARD -namespace Microsoft.Maui -{ - internal class HybridWebViewQueryStringHelper - { - public static string RemovePossibleQueryString(string? url) - { - if (string.IsNullOrEmpty(url)) - { - return string.Empty; - } - var indexOfQueryString = url.IndexOf('?', StringComparison.Ordinal); - return (indexOfQueryString == -1) - ? url - : url.Substring(0, indexOfQueryString); - } - - // TODO: Replace this - - /// - /// A simple utility that takes a URL, extracts the query string and returns a dictionary of key-value pairs. - /// Note that values are unescaped. Manually created URLs in JavaScript should use encodeURIComponent to escape values. - /// - /// - /// - public static Dictionary GetKeyValuePairs(string? url) - { - var result = new Dictionary(); - if (!string.IsNullOrEmpty(url)) - { - var query = new Uri(url).Query; - if (query != null && query.Length > 1) - { - result = query - .Substring(1) - .Split('&') - .Select(p => p.Split('=')) - .ToDictionary(p => p[0], p => Uri.UnescapeDataString(p[1])); - } - } - - return result; - } - } -} -#endif diff --git a/src/Core/src/Platform/Android/AsyncStream.cs b/src/Core/src/Platform/Android/AsyncStream.cs new file mode 100644 index 000000000000..8110fa1a18b4 --- /dev/null +++ b/src/Core/src/Platform/Android/AsyncStream.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Maui.Platform; + +/// +/// Represents a stream that reads data asynchronously from a task that +/// produces either a byte array or a stream. +/// This class is useful for scenarios where the data source is not immediately +/// available and needs to be fetched asynchronously. +/// +/// Specifically, the Android WebView requires that you provide a stream +/// immediately, but the data may not be available until later. +/// This class allows you to wrap a task that fetches the data and provides +/// an asynchronous stream interface to read from it. +/// +class AsyncStream : Stream +{ + readonly Task _streamTask; + readonly ILogger? _logger; + Stream? _stream; + bool _isDisposed; + + public AsyncStream(Task byteArrayTask, ILogger? logger) + : this(AsStreamTask(byteArrayTask), logger) + { + } + + public AsyncStream(Task streamTask, ILogger? logger) + { + _streamTask = streamTask ?? throw new ArgumentNullException(nameof(streamTask)); + _logger = logger; + } + + static async Task AsStreamTask(Task task) + { + var bytes = await task; + if (bytes is null) + return Stream.Null; + return new MemoryStream(bytes); + } + + async Task GetStreamAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_isDisposed, nameof(AsyncStream)); + + if (_stream != null) + return _stream; + + cancellationToken.ThrowIfCancellationRequested(); + + _stream = await _streamTask.ConfigureAwait(false); + return _stream; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + try + { + var stream = await GetStreamAsync(cancellationToken).ConfigureAwait(false); + if (stream is null) + return 0; + return await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error reading from asynchronous stream: {ErrorMessage}", ex.Message); + throw; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + try + { + var stream = GetStreamAsync().GetAwaiter().GetResult(); + if (stream is null) + return 0; + return stream.Read(buffer, offset, count); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error reading from asynchronous stream: {ErrorMessage}", ex.Message); + throw; + } + } + + public override void Flush() => throw new NotSupportedException(); + + public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override bool CanRead => !_isDisposed; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + _stream?.Dispose(); + + _isDisposed = true; + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + return; + + if (_stream is not null) + await _stream.DisposeAsync().ConfigureAwait(false); + + _isDisposed = true; + await base.DisposeAsync().ConfigureAwait(false); + } +} diff --git a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs index 7aade3db85ce..42c90e2c45cc 100644 --- a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs +++ b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs @@ -5,11 +5,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.IO.Pipelines; using System.Text; -using System.Threading.Tasks; using System.Web; using Android.Webkit; +using Java.Net; using Microsoft.Extensions.Logging; using AWebView = Android.Webkit.WebView; @@ -32,39 +31,85 @@ public MauiHybridWebViewClient(HybridWebViewHandler handler) public override WebResourceResponse? ShouldInterceptRequest(AWebView? view, IWebResourceRequest? request) { - var response = GetResponseStream(view, request); + var url = request?.Url?.ToString(); - if (response is not null) + var logger = Handler?.MauiContext?.CreateLogger(); + + logger?.LogDebug("Intercepting request for {Url}.", url); + + if (view is not null && request is not null && !string.IsNullOrEmpty(url)) { - return response; + // 1. Check if the app wants to modify or override the request + var response = TryInterceptResponseStream(view, request, url, logger); + if (response is not null) + { + return response; + } + + // 2. Check if the request is for a local resource + response = GetResponseStream(view, request, url, logger); + if (response is not null) + { + return response; + } } + // 3. Otherwise, we let the request go through as is + logger?.LogDebug("Request for {Url} was not handled.", url); + return base.ShouldInterceptRequest(view, request); } - private WebResourceResponse? GetResponseStream(AWebView? view, IWebResourceRequest? request) + private WebResourceResponse? TryInterceptResponseStream(AWebView view, IWebResourceRequest request, string url, ILogger? logger) + { + if (Handler is null || Handler is IViewHandler ivh && ivh.VirtualView is null) + { + return null; + } + + // 1. First, create the event args + var platformArgs = new WebResourceRequestedEventArgs(view, request); + + // 2. Trigger the event for the app + var handled = Handler.VirtualView.WebResourceRequested(platformArgs); + + // 3. If the app reported that it completed the request, then we do nothing more + if (handled) + { + logger?.LogDebug("Request for {Url} was handled by the user.", url); + + return platformArgs.Response; + } + + return null; + } + + private WebResourceResponse? GetResponseStream(AWebView view, IWebResourceRequest request, string fullUrl, ILogger? logger) { if (Handler is null || Handler is IViewHandler ivh && ivh.VirtualView is null) { return null; } - var fullUrl = request?.Url?.ToString(); - var requestUri = HybridWebViewQueryStringHelper.RemovePossibleQueryString(fullUrl); + var requestUri = WebUtils.RemovePossibleQueryString(fullUrl); if (new Uri(requestUri) is not Uri uri || !HybridWebViewHandler.AppOriginUri.IsBaseOf(uri)) { return null; } + logger?.LogDebug("Request for {Url} will be handled by .NET MAUI.", fullUrl); + var relativePath = HybridWebViewHandler.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); // 1. Try special InvokeDotNet path if (relativePath == HybridWebViewHandler.InvokeDotNetPath) { - var fullUri = new Uri(fullUrl!); + logger?.LogDebug("Request for {Url} will be handled by the .NET method invoker.", fullUrl); + + var fullUri = new Uri(fullUrl); var invokeQueryString = HttpUtility.ParseQueryString(fullUri.Query); var contentBytesTask = Handler.InvokeDotNetAsync(invokeQueryString); - var responseStream = new DotNetInvokeAsyncStream(contentBytesTask, Handler); + var responseStream = new AsyncStream(contentBytesTask, logger); return new WebResourceResponse("application/json", "UTF-8", 200, "OK", GetHeaders("application/json"), responseStream); } @@ -80,7 +125,7 @@ public MauiHybridWebViewClient(HybridWebViewHandler handler) if (!HybridWebViewHandler.ContentTypeProvider.TryGetContentType(relativePath, out contentType!)) { contentType = "text/plain"; - Handler.MauiContext?.CreateLogger()?.LogWarning("Could not determine content type for '{relativePath}'", relativePath); + logger?.LogWarning("Could not determine content type for '{relativePath}'", relativePath); } } @@ -90,6 +135,7 @@ public MauiHybridWebViewClient(HybridWebViewHandler handler) if (contentStream is not null) { // 3.a. If something was found, return the content + logger?.LogDebug("Request for {Url} will return an app package file.", fullUrl); // TODO: We don't know the content length because Android doesn't tell us. Seems to work without it! @@ -97,12 +143,8 @@ public MauiHybridWebViewClient(HybridWebViewHandler handler) } // 3.b. Otherwise, return a 404 - var notFoundContent = "Resource not found (404)"; - - var notFoundByteArray = Encoding.UTF8.GetBytes(notFoundContent); - var notFoundContentStream = new MemoryStream(notFoundByteArray); - - return new WebResourceResponse("text/plain", "UTF-8", 404, "Not Found", GetHeaders("text/plain"), notFoundContentStream); + logger?.LogDebug("Request for {Url} could not be fulfilled.", fullUrl); + return new WebResourceResponse(null, "UTF-8", 404, "Not Found", null, null); } private Stream? PlatformOpenAppPackageFile(string filename) @@ -151,123 +193,5 @@ internal void Disconnect() { _handler.SetTarget(null); } - - private class DotNetInvokeAsyncStream : Stream - { - private const int PauseThreshold = 32 * 1024; - private const int ResumeThreshold = 16 * 1024; - - private readonly Task _task; - private readonly WeakReference _handler; - private readonly Pipe _pipe; - - private bool _isDisposed; - - private HybridWebViewHandler? Handler => _handler?.GetTargetOrDefault(); - - public override bool CanRead => !_isDisposed; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public DotNetInvokeAsyncStream(Task invokeTask, HybridWebViewHandler handler) - { - _task = invokeTask; - _handler = new(handler); - - _pipe = new Pipe(new PipeOptions( - pauseWriterThreshold: PauseThreshold, - resumeWriterThreshold: ResumeThreshold, - useSynchronizationContext: false)); - - InvokeMethodAndWriteBytes(); - } - - private async void InvokeMethodAndWriteBytes() - { - try - { - var data = await _task; - - // the stream or handler may be disposed after the method completes - ObjectDisposedException.ThrowIf(_isDisposed, nameof(DotNetInvokeAsyncStream)); - ArgumentNullException.ThrowIfNull(Handler, nameof(Handler)); - - // copy the data into the pipe - if (data is not null && data.Length > 0) - { - var memory = _pipe.Writer.GetMemory(data.Length); - data.CopyTo(memory); - _pipe.Writer.Advance(data.Length); - } - - _pipe.Writer.Complete(); - } - catch (Exception ex) - { - Handler?.MauiContext?.CreateLogger()?.LogError(ex, "Error invoking .NET method from JavaScript: {ErrorMessage}", ex.Message); - - _pipe.Writer.Complete(ex); - } - } - - public override void Flush() => - throw new NotSupportedException(); - - public override int Read(byte[] buffer, int offset, int count) - { - ArgumentNullException.ThrowIfNull(buffer, nameof(buffer)); - ArgumentOutOfRangeException.ThrowIfNegative(offset, nameof(offset)); - ArgumentOutOfRangeException.ThrowIfNegative(count, nameof(count)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(offset + count, buffer.Length, nameof(count)); - ObjectDisposedException.ThrowIf(_isDisposed, nameof(DotNetInvokeAsyncStream)); - - // this is a blocking read, so we need to wait for data to be available - var readResult = _pipe.Reader.ReadAsync().AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); - var slice = readResult.Buffer.Slice(0, Math.Min(count, readResult.Buffer.Length)); - - var bytesRead = 0; - foreach (var span in slice) - { - var bytesToCopy = Math.Min(count, span.Length); - span.CopyTo(new Memory(buffer, offset, bytesToCopy)); - offset += bytesToCopy; - count -= bytesToCopy; - bytesRead += bytesToCopy; - } - - _pipe.Reader.AdvanceTo(slice.End); - - return bytesRead; - } - - public override long Seek(long offset, SeekOrigin origin) => - throw new NotSupportedException(); - - public override void SetLength(long value) => - throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException(); - - protected override void Dispose(bool disposing) - { - _isDisposed = true; - - _pipe.Writer.Complete(); - _pipe.Reader.Complete(); - - base.Dispose(disposing); - } - } } } diff --git a/src/Core/src/Primitives/WebResourceRequestedEventArgs.cs b/src/Core/src/Primitives/WebResourceRequestedEventArgs.cs new file mode 100644 index 000000000000..c6b55e9868e8 --- /dev/null +++ b/src/Core/src/Primitives/WebResourceRequestedEventArgs.cs @@ -0,0 +1,101 @@ +namespace Microsoft.Maui; + +/// +/// Provides platform-specific information for the event. +/// +public class WebResourceRequestedEventArgs +{ +#if WINDOWS + + internal WebResourceRequestedEventArgs( + global::Microsoft.Web.WebView2.Core.CoreWebView2 sender, + global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs eventArgs) + { + Sender = sender; + RequestEventArgs = eventArgs; + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on Windows. + /// + public global::Microsoft.Web.WebView2.Core.CoreWebView2 Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on Windows. + /// + public global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs RequestEventArgs { get; } + +#elif IOS || MACCATALYST + + internal WebResourceRequestedEventArgs( + global::WebKit.WKWebView sender, + global::WebKit.IWKUrlSchemeTask urlSchemeTask) + { + Sender = sender; + UrlSchemeTask = urlSchemeTask; + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on iOS and Mac Catalyst. + /// + public global::WebKit.WKWebView Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on iOS and Mac Catalyst. + /// + public global::WebKit.IWKUrlSchemeTask UrlSchemeTask { get; } + +#elif ANDROID + + internal WebResourceRequestedEventArgs( + global::Android.Webkit.WebView sender, + global::Android.Webkit.IWebResourceRequest request) + { + Sender = sender; + Request = request; + } + + /// + /// Gets the native view attached to the event. + /// + /// + /// This is only available on Android. + /// + public global::Android.Webkit.WebView Sender { get; } + + /// + /// Gets the native event args attached to the event. + /// + /// + /// This is only available on Android. + /// + public global::Android.Webkit.IWebResourceRequest Request { get; } + + /// + /// Gets or sets the native response attached to the event. + /// + /// + /// This is only available on Android. + /// + public global::Android.Webkit.WebResourceResponse? Response { get; set; } + +#else + + internal WebResourceRequestedEventArgs() + { + } + +#endif +} diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 4dc6a8746ea7..99a78cc53360 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -57,6 +57,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -80,6 +81,11 @@ Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.RenderProcessGoneDetail.get -> Android.Webkit.RenderProcessGoneDetail? Microsoft.Maui.WebProcessTerminatedEventArgs.Sender.get -> Android.Views.View? +Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebResourceRequestedEventArgs.Request.get -> Android.Webkit.IWebResourceRequest! +Microsoft.Maui.WebResourceRequestedEventArgs.Response.get -> Android.Webkit.WebResourceResponse? +Microsoft.Maui.WebResourceRequestedEventArgs.Response.set -> void +Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> Android.Webkit.WebView! override Microsoft.Maui.Handlers.EditorHandler.ConnectHandler(Microsoft.Maui.Platform.MauiAppCompatEditText! platformView) -> void override Microsoft.Maui.Handlers.EditorHandler.CreatePlatformView() -> Microsoft.Maui.Platform.MauiAppCompatEditText! override Microsoft.Maui.Handlers.EditorHandler.DisconnectHandler(Microsoft.Maui.Platform.MauiAppCompatEditText! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 3e411bcc0303..eed7d2f29af5 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -75,6 +76,9 @@ Microsoft.Maui.Platform.UIWindowExtensions Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! override Microsoft.Maui.Handlers.HybridWebViewHandler.ConnectHandler(WebKit.WKWebView! platformView) -> void override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> WebKit.WKWebView! override Microsoft.Maui.Handlers.HybridWebViewHandler.DisconnectHandler(WebKit.WKWebView! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 486d4b279528..7da78ee61bd0 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -76,6 +77,9 @@ Microsoft.Maui.Platform.UIWindowExtensions Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! override Microsoft.Maui.Handlers.HybridWebViewHandler.ConnectHandler(WebKit.WKWebView! platformView) -> void override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> WebKit.WKWebView! override Microsoft.Maui.Handlers.HybridWebViewHandler.DisconnectHandler(WebKit.WKWebView! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 82f603bb4b91..61eee164a6e3 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -73,6 +74,9 @@ Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.CoreWebView2ProcessFailedEventArgs.get -> Microsoft.Web.WebView2.Core.CoreWebView2ProcessFailedEventArgs! Microsoft.Maui.WebProcessTerminatedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! +Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebResourceRequestedEventArgs.RequestEventArgs.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs! +Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! override Microsoft.Maui.Handlers.HybridWebViewHandler.ConnectHandler(Microsoft.UI.Xaml.Controls.WebView2! platformView) -> void override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> Microsoft.UI.Xaml.Controls.WebView2! override Microsoft.Maui.Handlers.HybridWebViewHandler.DisconnectHandler(Microsoft.UI.Xaml.Controls.WebView2! platformView) -> void diff --git a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt index fbe7261a4a33..58500097e73a 100644 --- a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -66,6 +67,7 @@ Microsoft.Maui.IWindow.Content.get -> Microsoft.Maui.IView? Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.WebProcessTerminatedEventArgs() -> void +Microsoft.Maui.WebResourceRequestedEventArgs override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> object! static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler, System.Type! type) -> T static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler) -> T diff --git a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index fbe7261a4a33..58500097e73a 100644 --- a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -66,6 +67,7 @@ Microsoft.Maui.IWindow.Content.get -> Microsoft.Maui.IView? Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.WebProcessTerminatedEventArgs() -> void +Microsoft.Maui.WebResourceRequestedEventArgs override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> object! static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler, System.Type! type) -> T static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler) -> T diff --git a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index fbe7261a4a33..58500097e73a 100644 --- a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.Maui.IHybridWebView.InvokeJavaScriptType.set -> void Microsoft.Maui.IHybridWebView.RawMessageReceived(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SendRawMessage(string! rawMessage) -> void Microsoft.Maui.IHybridWebView.SetInvokeJavaScriptTarget(T! target) -> void +Microsoft.Maui.IHybridWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool Microsoft.Maui.ITitleBar Microsoft.Maui.ITitleBar.PassthroughElements.get -> System.Collections.Generic.IList! Microsoft.Maui.ITitleBar.Subtitle.get -> string? @@ -66,6 +67,7 @@ Microsoft.Maui.IWindow.Content.get -> Microsoft.Maui.IView? Microsoft.Maui.TextAlignment.Justify = 3 -> Microsoft.Maui.TextAlignment Microsoft.Maui.WebProcessTerminatedEventArgs Microsoft.Maui.WebProcessTerminatedEventArgs.WebProcessTerminatedEventArgs() -> void +Microsoft.Maui.WebResourceRequestedEventArgs override Microsoft.Maui.Handlers.HybridWebViewHandler.CreatePlatformView() -> object! static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler, System.Type! type) -> T static Microsoft.Maui.ElementHandlerExtensions.GetRequiredService(this Microsoft.Maui.IElementHandler! handler) -> T diff --git a/src/Essentials/src/Types/Shared/WebUtils.shared.cs b/src/Essentials/src/Types/Shared/WebUtils.shared.cs index 2654b0b63e50..b01e1a9e76d8 100644 --- a/src/Essentials/src/Types/Shared/WebUtils.shared.cs +++ b/src/Essentials/src/Types/Shared/WebUtils.shared.cs @@ -1,12 +1,28 @@ +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.Maui.ApplicationModel +namespace Microsoft.Maui { static class WebUtils { - internal static IDictionary ParseQueryString(Uri uri) +#if !NETSTANDARD + internal static string RemovePossibleQueryString(string? url) + { + if (string.IsNullOrEmpty(url)) + { + return string.Empty; + } + + var indexOfQueryString = url.IndexOf('?', StringComparison.Ordinal); + return (indexOfQueryString == -1) + ? url + : url.Substring(0, indexOfQueryString); + } +#endif + + internal static Dictionary ParseQueryString(Uri uri, bool includeFragment = true) { var parameters = new Dictionary(StringComparer.Ordinal); @@ -17,9 +33,12 @@ internal static IDictionary ParseQueryString(Uri uri) if (!string.IsNullOrEmpty(uri.Query)) UnpackParameters(uri.Query.AsSpan(1), parameters); - // Note: Uri.Fragment starts with a '#' - if (!string.IsNullOrEmpty(uri.Fragment)) - UnpackParameters(uri.Fragment.AsSpan(1), parameters); + if (includeFragment) + { + // Note: Uri.Fragment starts with a '#' + if (!string.IsNullOrEmpty(uri.Fragment)) + UnpackParameters(uri.Fragment.AsSpan(1), parameters); + } return parameters; } @@ -29,7 +48,7 @@ internal static IDictionary ParseQueryString(Uri uri) // // 1. avoids the IEnumerable overhead that isn't needed (the ASP.NET logic was clearly designed that way to offer a public API whereas we don't need that) // 2. avoids the use of unsafe code - static void UnpackParameters(ReadOnlySpan query, Dictionary parameters) + internal static void UnpackParameters(ReadOnlySpan query, Dictionary parameters) { while (!query.IsEmpty) { diff --git a/src/Essentials/test/DeviceTests/Tests/WebAuthenticator_Tests.cs b/src/Essentials/test/DeviceTests/Tests/WebAuthenticator_Tests.cs index b93f6b0ca51c..06f150b3b067 100644 --- a/src/Essentials/test/DeviceTests/Tests/WebAuthenticator_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/WebAuthenticator_Tests.cs @@ -134,20 +134,6 @@ public async Task RedirectWithResponseDecoder_WithCancellation(string urlBase, s - [Theory] - [InlineData("xamarinessentials://#access_token=blah&refresh_token=blah2&expires=1", "blah", "blah2", "1")] - [InlineData("xamarinessentials://?access_token=blah&refresh_token=blah2&expires=1", "blah", "blah2", "1")] - [InlineData("xamarinessentials://?access_token=access+token+with+spaces&refresh_token=refresh%20token%20with%20spaces&expires=1", "access token with spaces", "refresh token with spaces", "1")] - [Trait(Traits.InteractionType, Traits.InteractionTypes.Human)] - public void ParseQueryString(string url, string accessToken, string refreshToken, string expires) - { - var r = WebUtils.ParseQueryString(new Uri(url)); - - Assert.Equal(accessToken, r?["access_token"]); - Assert.Equal(refreshToken, r?["refresh_token"]); - Assert.Equal(expires, r?["expires"]); - } - internal class TestResponseDecoder : IWebAuthenticatorResponseDecoder { internal int CallCount = 0; diff --git a/src/Essentials/test/DeviceTests/Tests/WebUtils_Tests.cs b/src/Essentials/test/DeviceTests/Tests/WebUtils_Tests.cs new file mode 100644 index 000000000000..7d55004d4489 --- /dev/null +++ b/src/Essentials/test/DeviceTests/Tests/WebUtils_Tests.cs @@ -0,0 +1,21 @@ +using System; +using Xunit; + +namespace Microsoft.Maui.Essentials.DeviceTests; + +[Category("WebAuthenticator")] +public class WebUtils_Tests +{ + [Theory] + [InlineData("xamarinessentials://#access_token=blah&refresh_token=blah2&expires=1", "blah", "blah2", "1")] + [InlineData("xamarinessentials://?access_token=blah&refresh_token=blah2&expires=1", "blah", "blah2", "1")] + [InlineData("xamarinessentials://?access_token=access+token+with+spaces&refresh_token=refresh%20token%20with%20spaces&expires=1", "access token with spaces", "refresh token with spaces", "1")] + public void ParseQueryString(string url, string accessToken, string refreshToken, string expires) + { + var r = WebUtils.ParseQueryString(new Uri(url)); + + Assert.Equal(accessToken, r?["access_token"]); + Assert.Equal(refreshToken, r?["refresh_token"]); + Assert.Equal(expires, r?["expires"]); + } +} diff --git a/src/Essentials/test/UnitTests/Browser_Tests.cs b/src/Essentials/test/UnitTests/Browser_Tests.cs index f1d32ce25714..21c106b34898 100644 --- a/src/Essentials/test/UnitTests/Browser_Tests.cs +++ b/src/Essentials/test/UnitTests/Browser_Tests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Maui; using Microsoft.Maui.ApplicationModel; using Xunit;