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;