File: Handlers\HybridWebView\HybridWebViewHandler.iOS.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using System.Web;
using Foundation;
using Microsoft.Extensions.Logging;
using UIKit;
using WebKit;
using RectangleF = CoreGraphics.CGRect;
 
namespace Microsoft.Maui.Handlers
{
	public partial class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
	{
		private const string ScriptMessageHandlerName = "webwindowinterop";
 
		protected override WKWebView CreatePlatformView()
		{
			var config = new WKWebViewConfiguration();
 
			// By default, setting inline media playback to allowed, including autoplay
			// and picture in picture, since these things MUST be set during the webview
			// creation, and have no effect if set afterwards.
			// A custom handler factory delegate could be set to disable these defaults
			// but if we do not set them here, they cannot be changed once the
			// handler's platform view is created, so erring on the side of wanting this
			// capability by default.
			if (OperatingSystem.IsMacCatalystVersionAtLeast(10) || OperatingSystem.IsIOSVersionAtLeast(10))
			{
				config.AllowsPictureInPictureMediaPlayback = true;
				config.AllowsInlineMediaPlayback = true;
				config.MediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypes.None;
			}
 
			config.DefaultWebpagePreferences!.AllowsContentJavaScript = true;
 
			config.UserContentController.AddScriptMessageHandler(new WebViewScriptMessageHandler(this), ScriptMessageHandlerName);
			// iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme
			config.SetUrlSchemeHandler(new SchemeHandler(this), urlScheme: "app");
 
			var webview = new MauiHybridWebView(this, RectangleF.Empty, config)
			{
				BackgroundColor = UIColor.Clear,
				AutosizesSubviews = true
			};
 
			if (DeveloperTools.Enabled)
			{
				// Legacy Developer Extras setting.
				config.Preferences.SetValueForKey(NSObject.FromObject(true), new NSString("developerExtrasEnabled"));
 
				if (OperatingSystem.IsIOSVersionAtLeast(16, 4) || OperatingSystem.IsMacCatalystVersionAtLeast(16, 6))
				{
					// Enable Developer Extras for iOS builds for 16.4+ and Mac Catalyst builds for 16.6 (macOS 13.5)+
					webview.SetValueForKey(NSObject.FromObject(true), new NSString("inspectable"));
				}
			}
 
			return webview;
		}
 
		internal static void EvaluateJavaScript(IHybridWebViewHandler handler, IHybridWebView hybridWebView, EvaluateJavaScriptAsyncRequest request)
		{
			handler.PlatformView.EvaluateJavaScript(request);
		}
 
		public static void MapSendRawMessage(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg)
		{
			if (arg is not HybridWebViewRawMessage hybridWebViewRawMessage || handler.PlatformView is not IHybridPlatformWebView hybridPlatformWebView)
			{
				return;
			}
 
			hybridPlatformWebView.SendRawMessage(hybridWebViewRawMessage.Message ?? "");
		}
 
		private void MessageReceived(Uri uri, string message)
		{
			MessageReceived(message);
		}
 
		protected override void ConnectHandler(WKWebView platformView)
		{
			base.ConnectHandler(platformView);
 
			using var nsUrl = new NSUrl(new Uri(AppOriginUri, "/").ToString());
			using var request = new NSUrlRequest(nsUrl);
 
			platformView.LoadRequest(request);
		}
 
		protected override void DisconnectHandler(WKWebView platformView)
		{
			platformView.Configuration.UserContentController.RemoveScriptMessageHandler(ScriptMessageHandlerName);
 
			base.DisconnectHandler(platformView);
		}
 
 
		[RequiresUnreferencedCode(DynamicFeatures)]
#if !NETSTANDARD
		[RequiresDynamicCode(DynamicFeatures)]
#endif
		private sealed class WebViewScriptMessageHandler : NSObject, IWKScriptMessageHandler
		{
			private readonly WeakReference<HybridWebViewHandler?> _webViewHandler;
 
			public WebViewScriptMessageHandler(HybridWebViewHandler webViewHandler)
			{
				_webViewHandler = new(webViewHandler);
			}
 
			private HybridWebViewHandler? Handler => _webViewHandler is not null && _webViewHandler.TryGetTarget(out var h) ? h : null;
 
			public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
			{
				ArgumentNullException.ThrowIfNull(message);
				Handler?.MessageReceived(AppOriginUri, ((NSString)message.Body).ToString());
			}
		}
 
		[RequiresUnreferencedCode(DynamicFeatures)]
#if !NETSTANDARD
		[RequiresDynamicCode(DynamicFeatures)]
#endif
		private class SchemeHandler : NSObject, IWKUrlSchemeHandler
		{
			private readonly WeakReference<HybridWebViewHandler?> _webViewHandler;
 
			public SchemeHandler(HybridWebViewHandler webViewHandler)
			{
				_webViewHandler = new(webViewHandler);
			}
 
			private HybridWebViewHandler? Handler => _webViewHandler is not null && _webViewHandler.TryGetTarget(out var h) ? h : null;
 
			// The `async void` is intentional here, as this is an event handler that represents the start
			// of a request for some data from the webview. Once the task is complete, the `IWKUrlSchemeTask`
			// object is used to send the response back to the webview.
			[Export("webView:startURLSchemeTask:")]
			[SupportedOSPlatform("ios11.0")]
			public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
			{
				if (Handler is null || Handler is IViewHandler ivh && ivh.VirtualView is null)
				{
					return;
				}
 
				var url = urlSchemeTask.Request.Url?.AbsoluteString ?? "";
 
				var (bytes, contentType, statusCode) = await GetResponseBytesAsync(url);
 
				if (statusCode == 200)
				{
					// the method was invoked successfully, so we need to send the response back to the webview
 
					using (var dic = new NSMutableDictionary<NSString, NSString>())
					{
						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");
 
						if (urlSchemeTask.Request.Url != null)
						{
							using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, statusCode, "HTTP/1.1", dic);
							urlSchemeTask.DidReceiveResponse(response);
						}
					}
 
					urlSchemeTask.DidReceiveData(NSData.FromArray(bytes));
					urlSchemeTask.DidFinish();
				}
				else
				{
					// there was an error, so we need to handle it
 
					Handler?.MauiContext?.CreateLogger<HybridWebViewHandler>()?.LogError("Failed to load URL: {url}", url);
				}
			}
 
			private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytesAsync(string? url)
			{
				if (Handler is null)
				{
					return (Array.Empty<byte>(), ContentType: string.Empty, StatusCode: 404);
				}
 
				var fullUrl = url;
				url = HybridWebViewQueryStringHelper.RemovePossibleQueryString(url);
 
				if (new Uri(url) is Uri uri && AppOriginUri.IsBaseOf(uri))
				{
					var relativePath = AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/');
 
					var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, Handler.VirtualView.HybridRoot!);
 
					// 1. Try special InvokeDotNet path
					if (relativePath == InvokeDotNetPath)
					{
						var fullUri = new Uri(fullUrl!);
						var invokeQueryString = HttpUtility.ParseQueryString(fullUri.Query);
						var contentBytes = await Handler.InvokeDotNetAsync(invokeQueryString);
						if (contentBytes is not null)
						{
							return (contentBytes, "application/json", StatusCode: 200);
						}
					}
 
					string contentType;
 
					// 2. If nothing found yet, try to get static content from the asset path
					if (string.IsNullOrEmpty(relativePath))
					{
						relativePath = Handler.VirtualView.DefaultFile!.Replace('\\', '/');
						contentType = "text/html";
					}
					else
					{
						if (!ContentTypeProvider.TryGetContentType(relativePath, out contentType!))
						{
							// TODO: Log this
							contentType = "text/plain";
						}
					}
 
					var assetPath = Path.Combine(bundleRootDir, relativePath);
 
					if (File.Exists(assetPath))
					{
						return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200);
					}
				}
 
				return (Array.Empty<byte>(), ContentType: string.Empty, StatusCode: 404);
			}
 
			[Export("webView:stopURLSchemeTask:")]
			public void StopUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
			{
			}
		}
	}
}