From 1dcbab61597bde3cc354a2e5725cde2a696e7484 Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Fri, 2 Aug 2024 13:47:11 -0700 Subject: [PATCH 1/8] Request intercept and proxy improvements - Added support for a lot more mime/content types. Maintained the same fallback values as before. - Improved support for MainFile such that it can be a URL with query string parameters. Allows the main file to point to the proxy. - Added support for custom response headers via the proxy. - Cleaned up some repetitive code to ensure more consistency between platforms. - Extended proxy sample to demonstrate how it can be used to access resources on non-CORs enabled endpoints. - Added an example wraps the HybridWebView in a class library to create a custom control. It uses the proxy to load the main page and an image as embedded resource. Added a page to the main sample that loads this custom control. - Improve stability of query string parsing --- .../EmbeddedImage.png | Bin 0 -> 15627 bytes .../EmbeddedWebPageResource.html | 45 ++++ .../ExampleWrappedHybridWebViewLibrary.csproj | 40 ++++ .../MyCustomControl.cs | 123 ++++++++++ .../Platforms/Android/PlatformClass1.cs | 7 + .../Platforms/MacCatalyst/PlatformClass1.cs | 7 + .../Platforms/Tizen/PlatformClass1.cs | 9 + .../Platforms/Windows/PlatformClass1.cs | 7 + .../Platforms/iOS/PlatformClass1.cs | 7 + HybridWebView/HybridWebViewProxyEventArgs.cs | 18 ++ HybridWebView/PathUtils.cs | 213 +++++++++++++++++- .../Android/AndroidHybridWebViewClient.cs | 107 ++++++--- .../HybridWebViewHandler.MacCatalyst.cs | 88 +++++--- .../Windows/HybridWebView.Windows.cs | 82 ++++--- .../Platforms/iOS/HybridWebViewHandler.iOS.cs | 88 +++++--- HybridWebView/QueryStringHelper.cs | 5 +- MauiCSharpInteropWebView.sln | 6 + MauiCSharpInteropWebView/AppShell.xaml | 10 +- .../EmbeddedResourceSample.xaml | 9 + .../EmbeddedResourceSample.xaml.cs | 13 ++ MauiCSharpInteropWebView/MainPage.xaml.cs | 31 +++ .../MauiCSharpInteropWebView.csproj | 12 + .../Resources/Raw/hybrid_root/proxy.html | 39 +++- 23 files changed, 827 insertions(+), 139 deletions(-) create mode 100644 ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png create mode 100644 ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html create mode 100644 ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj create mode 100644 ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs create mode 100644 ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs create mode 100644 ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs create mode 100644 ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs create mode 100644 ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs create mode 100644 ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs create mode 100644 MauiCSharpInteropWebView/EmbeddedResourceSample.xaml create mode 100644 MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs diff --git a/ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png b/ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png new file mode 100644 index 0000000000000000000000000000000000000000..aecf8d105334df112d150c5fa565959a2f9ddf56 GIT binary patch literal 15627 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 A9{>OV literal 0 HcmV?d00001 diff --git a/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html b/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html new file mode 100644 index 0000000..7adafcb --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html @@ -0,0 +1,45 @@ + + + + + + + + + + +

HybridWebView demo: Embedded Resource page

+
+ This web page is an embedded resource. It can access resources from in the + same way as normal via the HybridAssetRoot (not an embedded resrouce) and other embedded resources. + Native/JS communication works as expected. + This is useful when using the HybridWebView in a .NET Maui library when wrapping a web app that is + to be conusmed by other .NET Maui app. + +

+ +

+ +

+ +

+ The following image is loaded from an embedded resource via the proxy: +
+
+ +

+
+ + + + diff --git a/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj b/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj new file mode 100644 index 0000000..c2fdf7c --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj @@ -0,0 +1,40 @@ + + + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + + + true + true + enable + enable + + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + diff --git a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs new file mode 100644 index 0000000..f13b3fe --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs @@ -0,0 +1,123 @@ +using HybridWebView; + +namespace ExampleWrappedHybridWebViewLibrary +{ + /// + /// A custom control that wraps a hybrid web view and loads embedded resources in addition to supporting the + /// Hybrid asset root and all other capabilites of the HybridWebView. + /// + public class MyCustomControl: Grid + { + private readonly HybridWebView.HybridWebView _webView; + + #region Constructor + + public MyCustomControl() : base() + { + bool enableWebDevTools = false; + +#if DEBUG + //Enable web dev tools when in debug mode. + enableWebDevTools = true; +#endif + + //Create a web view control. + _webView = new HybridWebView.HybridWebView + { + HybridAssetRoot = HybridAssetRoot ?? "hybrid_root", + MainFile = "proxy?operation=embeddedResource&resourceName=EmbeddedWebPageResource.html", + EnableWebDevTools = enableWebDevTools + }; + + //Set the target for JavaScript interop. + _webView.JSInvokeTarget = new MyJSInvokeTarget(); + + //Monitor proxy requests. + _webView.ProxyRequestReceived += WebView_ProxyRequestReceived; + +#if WINDOWS + //In Windows, disable manual user zooming of web pages. + _webView.HybridWebViewInitialized += (s, e) => + { + //Disable the user manually zooming. Don't want the user accidentally zooming the HTML page. + e.WebView.CoreWebView2.Settings.IsZoomControlEnabled = false; + }; +#endif + + //Add the web view to the control. + this.Children.Insert(0, _webView); + } + + #endregion + + #region Public Properties + + public string? HybridAssetRoot { get; set; } = null; + + #endregion + + #region Private Methods + + private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxyEventArgs args) + { + // In an app, you might load responses from a sqlite database, zip file, or create files in memory (e.g. using SkiaSharp or System.Drawing) + + // Check to see if our custom parameter is present. + if (args.QueryParams.ContainsKey("operation")) + { + switch (args.QueryParams["operation"]) + { + case "embeddedResource": + if (args.QueryParams.TryGetValue("resourceName", out string? resourceName) && !string.IsNullOrWhiteSpace(resourceName)) + { + var thisAssembly = typeof(MyCustomControl).Assembly; + using (var fs = thisAssembly.GetManifestResourceStream("ExampleWrappedHybridWebViewLibrary." + resourceName.Replace("/", "."))) + { + if (fs != null) + { + var ms = new MemoryStream(); + await fs.CopyToAsync(ms); + ms.Position = 0; + + args.ResponseStream = ms; + args.ResponseContentType = PathUtils.GetMimeType(resourceName); + } + } + } + break; + default: + break; + } + } + } + + private sealed class MyJSInvokeTarget + { + public MyJSInvokeTarget() + { + } + + /// + /// An example of a round trip method that takes an input parameter and returns a simple value type (number). + /// + /// + /// + public double Fibonacci(int n) + { + if (n == 0) return 0; + + int prev = 0; + int next = 1; + for (int i = 1; i < n; i++) + { + int sum = prev + next; + prev = next; + next = sum; + } + return next; + } + } + + #endregion + } +} diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs new file mode 100644 index 0000000..f822afc --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs @@ -0,0 +1,7 @@ +namespace ExampleWrappedHybridWebViewLibrary +{ + // All the code in this file is only included on Android. + public class PlatformClass1 + { + } +} diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs new file mode 100644 index 0000000..98e6f32 --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs @@ -0,0 +1,7 @@ +namespace ExampleWrappedHybridWebViewLibrary +{ + // All the code in this file is only included on Mac Catalyst. + public class PlatformClass1 + { + } +} diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs new file mode 100644 index 0000000..5bc2f1e --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs @@ -0,0 +1,9 @@ +using System; + +namespace ExampleWrappedHybridWebViewLibrary +{ + // All the code in this file is only included on Tizen. + public class PlatformClass1 + { + } +} \ No newline at end of file diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs new file mode 100644 index 0000000..f21c4ea --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs @@ -0,0 +1,7 @@ +namespace ExampleWrappedHybridWebViewLibrary +{ + // All the code in this file is only included on Windows. + public class PlatformClass1 + { + } +} diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs new file mode 100644 index 0000000..68b9c82 --- /dev/null +++ b/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs @@ -0,0 +1,7 @@ +namespace ExampleWrappedHybridWebViewLibrary +{ + // All the code in this file is only included on iOS. + public class PlatformClass1 + { + } +} diff --git a/HybridWebView/HybridWebViewProxyEventArgs.cs b/HybridWebView/HybridWebViewProxyEventArgs.cs index 0f6299d..77385f1 100644 --- a/HybridWebView/HybridWebViewProxyEventArgs.cs +++ b/HybridWebView/HybridWebViewProxyEventArgs.cs @@ -15,6 +15,18 @@ public HybridWebViewProxyEventArgs(string fullUrl) QueryParams = QueryStringHelper.GetKeyValuePairs(fullUrl); } + /// + /// Creates a new instance of . + /// + /// The full request URL. + /// The estimated response content type based on uri parsing. + public HybridWebViewProxyEventArgs(string fullUrl, string contentType) + { + Url = fullUrl; + ResponseContentType = contentType; + QueryParams = QueryStringHelper.GetKeyValuePairs(fullUrl); + } + /// /// The full request URL. /// @@ -35,5 +47,11 @@ public HybridWebViewProxyEventArgs(string fullUrl) /// The response stream to be used to respond to the request. /// public Stream? ResponseStream { get; set; } = null; + + /// + /// Additional headers to be added to the response. + /// Useful for things like adding a cache control header. + /// + public IDictionary? CustomResponseHeaders { get; set; } } } diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index 2a92cee..b275f72 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -1,10 +1,219 @@ -namespace HybridWebView +using System; + +namespace HybridWebView { - internal static class PathUtils + public static class PathUtils { + public const string PlanTextMimeType = "text/plain"; + public const string HtmlMimeType = "text/html"; + public static string NormalizePath(string filename) => filename .Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar); + + public static void GetRelativePathAndContentType(Uri appOriginUri, Uri requestUri, string originalUrl, string? mainFileName, out string? relativePath, out string contentType, out string fullUrl) + { + relativePath = appOriginUri.MakeRelativeUri(requestUri).ToString().Replace('/', '\\'); + fullUrl = originalUrl; + + if (string.IsNullOrEmpty(relativePath)) + { + //The main file may be a URL that has a query string. For example if we want the main page to go through the proxy. + if (!string.IsNullOrEmpty(mainFileName)) + { + relativePath = QueryStringHelper.RemovePossibleQueryString(mainFileName); + fullUrl = mainFileName; + } + + //Try and get the mime type from the full URL (main file could be a URL that has a query string pointing to a file such as a PDF). + string? minType; + if (PathUtils.TryGetMimeType(fullUrl, out minType)) + { + if (!string.IsNullOrEmpty(minType)) + { + contentType = minType; + } + else + { + contentType = PathUtils.HtmlMimeType; + } + } + else + { + contentType = PathUtils.HtmlMimeType; + } + } + else + { + contentType = PathUtils.GetMimeType(fullUrl); + } + } + + /// + /// Tries to get the mime type from a file name or URL by looking for valid file extensions or mime types in a data URI. + /// Input can be a file name, url (with query string), a data uri, mime type, or file extension. + /// + /// A file name, url (with query string), a data uri, mime type, or file extension. + /// The determined mime type. Fallback to "text/plain" + public static string GetMimeType(string fileNameOrUrl) + { + string? ext; + string? mimeType; + + //Check for a mime type in a data uri. + if (fileNameOrUrl.Contains("data:") && fileNameOrUrl.Contains(";base64,")) + { + ext = fileNameOrUrl.Substring(5, fileNameOrUrl.IndexOf(";base64,") - 5); + + if (TryGetMimeType(ext, out mimeType)) + { + return mimeType ?? PlanTextMimeType; + } + } + + //Seperate out query string if it exists. + string queryString = string.Empty; + + if (fileNameOrUrl.Contains("?")) + { + queryString = fileNameOrUrl.Substring(fileNameOrUrl.IndexOf("?")); + fileNameOrUrl = fileNameOrUrl.Substring(0, fileNameOrUrl.IndexOf("?")); + } + + //If there is still a url or file name, check it for a valid file extension. + if (!string.IsNullOrWhiteSpace(fileNameOrUrl)) + { + ext = Path.GetExtension(fileNameOrUrl); + + if (TryGetMimeType(ext, out mimeType)) + { + return mimeType ?? PlanTextMimeType; + } + + //Try passing the whole file name to see if it is itself a valid mime type. This would work if only a file extension or mimetype was passed in. + if (TryGetMimeType(fileNameOrUrl, out mimeType)) + { + return mimeType ?? PlanTextMimeType; + } + } + + //If there is a query string, check it's parameter values to see if it contains something with a valid file extension. + if (!string.IsNullOrWhiteSpace(queryString)) + { + var queryParameters = queryString.Split('&'); + + foreach (var param in queryParameters) + { + if (param.Contains("=")) + { + ext = param.Substring(param.IndexOf("=") + 1); + + if (TryGetMimeType(ext, out mimeType)) + { + return mimeType ?? PlanTextMimeType; + } + } + } + } + + //If get here, return plain text mime type. + return PlanTextMimeType; + } + + /// + /// Looks up a mime type based on a file extension or mime type. + /// + /// A mimeType or file extension to validate and get the mime type for. + /// A boolean indicating if it found a supported mime type. + public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? mimeType) + { + mimeType = null; + + if (string.IsNullOrWhiteSpace(mimeTypeOrFileExtension)) + { + return false; + } + + //If content type starts with a period, remove it. File extension may have been passed in. + if (mimeTypeOrFileExtension.StartsWith(".")) + { + mimeTypeOrFileExtension = mimeTypeOrFileExtension.Substring(1); + } + + //For simplirity, if the content type contains a slash, assume it is a file path extension. + if (mimeTypeOrFileExtension.Contains("/")) + { + //Return the string after the last index of the slash. + mimeTypeOrFileExtension = mimeTypeOrFileExtension.Substring(mimeTypeOrFileExtension.LastIndexOf("/") + 1); + } + + //Sanitize the content type. + mimeType = mimeTypeOrFileExtension.ToLowerInvariant() switch + { + //Image file types + "png" => "image/png", + "jpg" or "jpeg" or "jfif" or "pjpeg" or "pjp" => "image/jpeg", + "gif" => "image/gif", + "webp" => "image/wemp", + "svg" or "svg+xml" => "image/svg+xml", + "ico" or "x-icon" => "image/x-icon", + "bmp" => "image/bmp", + "tif" or "tiff" => "image/tiff", + "avif" => "image/avif", + "apng" => "image/apng", + + //Video file types + "mp4" => "video/mp4", + "webm" => "video/webm", + "mpeg" => "video/mpeg", + + //Audio file types + "mp3" => "audio/mpeg", + "wav" => "audio/wav", + + //Font file types + "woff" => "font/woff", + "woff2" => "font/woff2", + "otf" => "font/otf", + + //JSON and XML based file types + "json" or "geojson" or "geojsonseq" or "topojson" => "application/json", + "gpx" or "georss" or "gml" or "citygml" or "czml" or "xml" => "application/xml", + "kml" or "kml+xml" or "vnd.google-earth.kml+xml" => "application/vnd.google-earth.kml+xml", + + //Office file types + "doc" or "docx" or "msword" => "application/msword", + "xls" or "xlsx" or "vnd.ms-excel" => "application/vnd.ms-excel", + "ppt" or "pptx" or "vnd.ms-powerpoint" => "application/vnd.ms-powerpoint", + + //3D model file types commonly used in web. + "gltf" or "gltf+json" => "model/gltf+json", + "glb" or "gltf-binary" => "model/gltf-binary", + "dae" => "model/vnd.collada+xml", + + //Other binary file types + "zip" => "application/zip", + "pbf" or "x-protobuf" => "application/x-protobuf", + "kmz" or "vnd.google-earth.kmz" or "shp" or "dbf" or "bin" or "b3dm" or "i3dm" or "pnts" or "subtree" or "octet-stream" => "application/octet-stream", + "pdf" => "application/pdf", + + //Other map tile file types + "terrian" => "application/vnd.quantized-mesh", + "pmtiles" => "application/vnd.pmtiles", + + //Text based file types + "htm" or "html" => "text/html", + "xhtml" => "application/xhtml+xml", + "js" or "javascript" => "text/javascript", + "css" => "text/css", + "csv" => "text/csv", + "plain" or "txt" => "text/plain", + + _ => null, + }; + + return mimeType != null; + } } } diff --git a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs index f0f6619..8d45344 100644 --- a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs +++ b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs @@ -14,53 +14,76 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler) { _handler = handler; } + public override WebResourceResponse? ShouldInterceptRequest(AWebView? view, IWebResourceRequest? request) { - var fullUrl = request?.Url?.ToString(); - var requestUri = QueryStringHelper.RemovePossibleQueryString(fullUrl); + var originalUrl = request?.Url?.ToString(); + var requestUri = QueryStringHelper.RemovePossibleQueryString(originalUrl); var webView = (HybridWebView)_handler.VirtualView; - if (new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) + if (!string.IsNullOrEmpty(originalUrl) && new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { - var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); - - string contentType; - if (string.IsNullOrEmpty(relativePath)) - { - relativePath = webView.MainFile; - contentType = "text/html"; - } - else - { - var requestExtension = Path.GetExtension(relativePath); - contentType = requestExtension switch - { - ".htm" or ".html" => "text/html", - ".js" => "application/javascript", - ".css" => "text/css", - _ => "text/plain", - }; - } + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, webView.MainFile, out string? relativePath, out string contentType, out string fullUrl); + //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); + + //IDictionary? customHeaders = null; + + //string contentType; + //if (string.IsNullOrEmpty(relativePath)) + //{ + // //The main file may be a URL that has a query string. Fpor example if we want the main page to go through the proxy. + // if (!string.IsNullOrEmpty(webView.MainFile) && webView.MainFile.Contains("?")) + // { + // relativePath = QueryStringHelper.RemovePossibleQueryString(relativePath); + // } + // else + // { + // relativePath = webView.MainFile; + // } + + // //Try and get the mime type from the full URL (main file could be a URL that has a query string pointing to a file such as a PDF). + // string? minType; + // if(PathUtils.TryGetMimeType(fullUrl, out minType)) + // { + // if (!string.IsNullOrEmpty(minType)) + // { + // contentType = minType; + // } else + // { + // contentType = PathUtils.HtmlMimeType; + // } + // } + // else + // { + // contentType = PathUtils.HtmlMimeType; + // } + //} + //else + //{ + // contentType = PathUtils.GetMimeType(fullUrl); + //} Stream? contentStream = null; + IDictionary? customHeaders = null; // Check to see if the request is a proxy request. - if (relativePath == HybridWebView.ProxyRequestPath) + if (!string.IsNullOrEmpty(relativePath) && relativePath.Equals(HybridWebView.ProxyRequestPath)) { - var args = new HybridWebViewProxyEventArgs(fullUrl); + var args = new HybridWebViewProxyEventArgs(fullUrl, contentType); // TODO: Don't block async. Consider making this an async call, and then calling DidFinish when done webView.OnProxyRequestMessage(args).Wait(); if (args.ResponseStream != null) { - contentType = args.ResponseContentType ?? "text/plain"; + contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType; contentStream = args.ResponseStream; + customHeaders = args.CustomResponseHeaders; } } - if (contentStream == null) + if (contentStream is null) { contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!); } @@ -78,12 +101,12 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler) 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); + return new WebResourceResponse("text/plain", "UTF-8", 404, "Not Found", GetHeaders("text/plain", null), notFoundContentStream); } else { // TODO: We don't know the content length because Android doesn't tell us. Seems to work without it! - return new WebResourceResponse(contentType, "UTF-8", 200, "OK", GetHeaders(contentType), contentStream); + return new WebResourceResponse(contentType, "UTF-8", 200, "OK", GetHeaders(contentType, customHeaders), contentStream); } } else @@ -106,9 +129,29 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler) } } - private protected static IDictionary GetHeaders(string contentType) => - new Dictionary { - { "Content-Type", contentType }, - }; + private protected static IDictionary GetHeaders(string contentType, IDictionary? customHeaders) + { + var headers = new Dictionary(); + + if (customHeaders != null) + { + foreach (var header in customHeaders) + { + // Add custom headers to the response. Skip the Content-Length and Content-Type headers. + if (header.Key != "Content-Length" && header.Key != "Content-Type") + { + headers[header.Key] = header.Value; + } + } + } + + //If a custom header hasn't specified a content type, use the one we've determined. + if (!headers.ContainsKey("Content-Type")) + { + headers.Add("Content-Type", contentType); + } + + return headers; + } } } diff --git a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs index d31f86f..f81665c 100644 --- a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs +++ b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs @@ -1,5 +1,7 @@ using Foundation; +using Intents; using Microsoft.Maui.Platform; +using System; using System.Drawing; using System.Globalization; using System.Runtime.Versioning; @@ -78,8 +80,26 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { dic.Add((NSString)"Content-Length", (NSString)(responseData.ResponseBytes.Length.ToString(CultureInfo.InvariantCulture))); dic.Add((NSString)"Content-Type", (NSString)responseData.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 (responseData.CustomHeaders != null) + { + foreach (var header in responseData.CustomHeaders) + { + // Add custom headers to the response. Skip the Content-Length and Content-Type headers. + if (header.Key != "Content-Length" && header.Key != "Content-Type") + { + dic.Add((NSString)header.Key, (NSString)header.Value); + } + } + } + + //Ensure that the Cache-Control header is not set in the custom headers. + if(responseData.CustomHeaders == null || responseData.CustomHeaders.ContainsKey("Cache-Control")) + { + // 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, responseData.StatusCode, "HTTP/1.1", dic); @@ -92,55 +112,54 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche } } - private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytes(string? url) + private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode, IDictionary? CustomHeaders)> GetResponseBytes(string? url) { - string contentType; + //string contentType; - string fullUrl = url; + string? originalUrl = url; url = QueryStringHelper.RemovePossibleQueryString(url); - if (new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) + if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { - var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); - var hwv = (HybridWebView)_webViewHandler.VirtualView; - - var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); - - if (string.IsNullOrEmpty(relativePath)) - { - relativePath = hwv.MainFile!.Replace('\\', '/'); - contentType = "text/html"; - } - else - { - var requestExtension = Path.GetExtension(relativePath); - contentType = requestExtension switch - { - ".htm" or ".html" => "text/html", - ".js" => "application/javascript", - ".css" => "text/css", - _ => "text/plain", - }; - } + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); + //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); + + //if (string.IsNullOrEmpty(relativePath)) + //{ + // relativePath = hwv.MainFile!.Replace('\\', '/'); + // contentType = "text/html"; + //} + //else + //{ + // var requestExtension = Path.GetExtension(relativePath); + // contentType = requestExtension switch + // { + // ".htm" or ".html" => "text/html", + // ".js" => "application/javascript", + // ".css" => "text/css", + // _ => "text/plain", + // }; + //} Stream? contentStream = null; + IDictionary? customHeaders = null; // Check to see if the request is a proxy request. if (relativePath == HybridWebView.ProxyRequestPath) { - var args = new HybridWebViewProxyEventArgs(fullUrl); - + var args = new HybridWebViewProxyEventArgs(fullUrl, contentType); await hwv.OnProxyRequestMessage(args); if (args.ResponseStream != null) { - contentType = args.ResponseContentType ?? "text/plain"; + contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType; contentStream = args.ResponseStream; + customHeaders = args.CustomResponseHeaders; } } - if (contentStream == null) + if (contentStream is null) { contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!); } @@ -149,18 +168,19 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { using var ms = new MemoryStream(); contentStream.CopyTo(ms); - return (ms.ToArray(), contentType, StatusCode: 200); + return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders); } + var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); var assetPath = Path.Combine(bundleRootDir, relativePath); if (File.Exists(assetPath)) { - return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200); + return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200, CustomHeaders: null); } } - return (Array.Empty(), ContentType: string.Empty, StatusCode: 404); + return (Array.Empty(), ContentType: string.Empty, StatusCode: 404, CustomHeaders: null); } [Export("webView:stopURLSchemeTask:")] diff --git a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs index dfa037e..9aaae68 100644 --- a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs +++ b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs @@ -51,40 +51,41 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri)) { - var relativePath = AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); - - string contentType; - if (string.IsNullOrEmpty(relativePath)) - { - relativePath = MainFile; - contentType = "text/html"; - } - else - { - var requestExtension = Path.GetExtension(relativePath); - contentType = requestExtension switch - { - ".htm" or ".html" => "text/html", - ".js" => "application/javascript", - ".css" => "text/css", - _ => "text/plain", - }; - } + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string? relativePath, out string contentType, out string fullUrl); + //var relativePath = AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); + + //string contentType; + //if (string.IsNullOrEmpty(relativePath)) + //{ + // relativePath = MainFile; + // contentType = "text/html"; + //} + //else + //{ + // var requestExtension = Path.GetExtension(relativePath); + // contentType = requestExtension switch + // { + // ".htm" or ".html" => "text/html", + // ".js" => "application/javascript", + // ".css" => "text/css", + // _ => "text/plain", + // }; + //} Stream? contentStream = null; + IDictionary? customHeaders = null; // Check to see if the request is a proxy request if (relativePath == ProxyRequestPath) { - var fullUrl = eventArgs.Request.Uri; - - var args = new HybridWebViewProxyEventArgs(fullUrl); + var args = new HybridWebViewProxyEventArgs(fullUrl, contentType); await OnProxyRequestMessage(args); if (args.ResponseStream != null) { - contentType = args.ResponseContentType ?? "text/plain"; + contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType; contentStream = args.ResponseStream; + customHeaders = args.CustomResponseHeaders; } } @@ -106,7 +107,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe Content: null, StatusCode: 404, ReasonPhrase: "Not Found", - Headers: GetHeaderString("text/plain", notFoundContent.Length) + Headers: GetHeaderString("text/plain", notFoundContent.Length, null) ); } else @@ -115,7 +116,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe Content: await CopyContentToRandomAccessStreamAsync(contentStream), StatusCode: 200, ReasonPhrase: "OK", - Headers: GetHeaderString(contentType, (int)contentStream.Length) + Headers: GetHeaderString(contentType, (int)contentStream.Length, customHeaders) ); } @@ -135,9 +136,34 @@ async Task CopyContentToRandomAccessStreamAsync(Stream cont } } - private protected static string GetHeaderString(string contentType, int contentLength) => -$@"Content-Type: {contentType} -Content-Length: {contentLength}"; + private protected static string GetHeaderString(string contentType, int contentLength, IDictionary? customHeaders) + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine($"Content-Type: {contentType}"); + sb.AppendLine($"Content-Length: {contentLength}"); + + if (customHeaders != null) + { + foreach (var header in customHeaders) + { + // Add custom headers to the response. Skip the Content-Length and Content-Type headers. + if (header.Key != "Content-Length" && header.Key != "Content-Type") + { + sb.AppendLine($"{header.Key}: {header.Value}"); + } + } + } + + // Ensure that the Cache-Control header is not set in the custom headers. + if (customHeaders == null || !customHeaders.ContainsKey("Cache-Control")) + { + // Disable local caching. This will prevent user scripts from executing correctly. + sb.AppendLine("Cache-Control: no-cache, max-age=0, must-revalidate, no-store"); + } + + return sb.ToString(); + } private void Wv2_WebMessageReceived(Microsoft.UI.Xaml.Controls.WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) { diff --git a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs index a5fcf9f..244dbba 100644 --- a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs +++ b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs @@ -1,5 +1,7 @@ using Foundation; +using Intents; using Microsoft.Maui.Platform; +using System; using System.Drawing; using System.Globalization; using System.Reflection.Metadata; @@ -79,8 +81,26 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { dic.Add((NSString)"Content-Length", (NSString)(responseData.ResponseBytes.Length.ToString(CultureInfo.InvariantCulture))); dic.Add((NSString)"Content-Type", (NSString)responseData.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 (responseData.CustomHeaders != null) + { + foreach (var header in responseData.CustomHeaders) + { + // Add custom headers to the response. Skip the Content-Length and Content-Type headers. + if (header.Key != "Content-Length" && header.Key != "Content-Type") + { + dic.Add((NSString)header.Key, (NSString)header.Value); + } + } + } + + //Ensure that the Cache-Control header is not set in the custom headers. + if (responseData.CustomHeaders == null || responseData.CustomHeaders.ContainsKey("Cache-Control")) + { + // 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, responseData.StatusCode, "HTTP/1.1", dic); @@ -93,55 +113,56 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche } } - private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytes(string? url) + private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode, IDictionary? CustomHeaders)> GetResponseBytes(string? url) { - string contentType; + //string contentType; - string fullUrl = url; + string? originalUrl = url; url = QueryStringHelper.RemovePossibleQueryString(url); - if (new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) + if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { - var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); - var hwv = (HybridWebView)_webViewHandler.VirtualView; - - var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); - - if (string.IsNullOrEmpty(relativePath)) - { - relativePath = hwv.MainFile!.Replace('\\', '/'); - contentType = "text/html"; - } - else - { - var requestExtension = Path.GetExtension(relativePath); - contentType = requestExtension switch - { - ".htm" or ".html" => "text/html", - ".js" => "application/javascript", - ".css" => "text/css", - _ => "text/plain", - }; - } + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); + + //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); + + //if (string.IsNullOrEmpty(relativePath)) + //{ + // relativePath = hwv.MainFile!.Replace('\\', '/'); + // contentType = "text/html"; + //} + //else + //{ + // var requestExtension = Path.GetExtension(relativePath); + // contentType = requestExtension switch + // { + // ".htm" or ".html" => "text/html", + // ".js" => "application/javascript", + // ".css" => "text/css", + // _ => "text/plain", + // }; + //} Stream? contentStream = null; + IDictionary? customHeaders = null; // Check to see if the request is a proxy request. if (relativePath == HybridWebView.ProxyRequestPath) { - var args = new HybridWebViewProxyEventArgs(fullUrl); + var args = new HybridWebViewProxyEventArgs(fullUrl, contentType); await hwv.OnProxyRequestMessage(args); if (args.ResponseStream != null) { - contentType = args.ResponseContentType ?? "text/plain"; + contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType; contentStream = args.ResponseStream; + customHeaders = args.CustomResponseHeaders; } } - if (contentStream == null) + if (contentStream is null) { contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!); } @@ -150,18 +171,19 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { using var ms = new MemoryStream(); contentStream.CopyTo(ms); - return (ms.ToArray(), contentType, StatusCode: 200); + return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders); } + var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); var assetPath = Path.Combine(bundleRootDir, relativePath); if (File.Exists(assetPath)) { - return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200); + return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200, CustomHeaders: null); } } - return (Array.Empty(), ContentType: string.Empty, StatusCode: 404); + return (Array.Empty(), ContentType: string.Empty, StatusCode: 404, CustomHeaders: null); } [Export("webView:stopURLSchemeTask:")] diff --git a/HybridWebView/QueryStringHelper.cs b/HybridWebView/QueryStringHelper.cs index 007cc12..3780c53 100644 --- a/HybridWebView/QueryStringHelper.cs +++ b/HybridWebView/QueryStringHelper.cs @@ -27,11 +27,10 @@ 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) + string query = url.Substring(url.IndexOf("?") + 1); + if (!string.IsNullOrWhiteSpace(query)) { result = query - .Substring(1) .Split('&') .Select(p => p.Split('=')) .ToDictionary(p => p[0], p => Uri.UnescapeDataString(p[1])); diff --git a/MauiCSharpInteropWebView.sln b/MauiCSharpInteropWebView.sln index ce38ce3..48e39a6 100644 --- a/MauiCSharpInteropWebView.sln +++ b/MauiCSharpInteropWebView.sln @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWrappedHybridWebViewLibrary", "ExampleWrappedHybridWebViewLibrary\ExampleWrappedHybridWebViewLibrary.csproj", "{7C75B779-B7C2-4321-AE13-24470017AFC2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.Build.0 = Release|Any CPU {9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.Deploy.0 = Release|Any CPU + {7C75B779-B7C2-4321-AE13-24470017AFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C75B779-B7C2-4321-AE13-24470017AFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C75B779-B7C2-4321-AE13-24470017AFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C75B779-B7C2-4321-AE13-24470017AFC2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MauiCSharpInteropWebView/AppShell.xaml b/MauiCSharpInteropWebView/AppShell.xaml index 212a40d..e6322ec 100644 --- a/MauiCSharpInteropWebView/AppShell.xaml +++ b/MauiCSharpInteropWebView/AppShell.xaml @@ -4,11 +4,11 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MauiCSharpInteropWebView" - Shell.FlyoutBehavior="Disabled"> + Shell.FlyoutBehavior="Flyout"> - + + + + diff --git a/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml new file mode 100644 index 0000000..6d70ff5 --- /dev/null +++ b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs new file mode 100644 index 0000000..507f7a0 --- /dev/null +++ b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs @@ -0,0 +1,13 @@ +using HybridWebView; +using System.Globalization; +using System.IO.Compression; + +namespace MauiCSharpInteropWebView; + +public partial class EmbeddedResourceSample : ContentPage +{ + public EmbeddedResourceSample() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/MauiCSharpInteropWebView/MainPage.xaml.cs b/MauiCSharpInteropWebView/MainPage.xaml.cs index 9a528dd..be65e34 100644 --- a/MauiCSharpInteropWebView/MainPage.xaml.cs +++ b/MauiCSharpInteropWebView/MainPage.xaml.cs @@ -1,4 +1,5 @@ using HybridWebView; +using System.Diagnostics; using System.Globalization; using System.IO.Compression; using System.Text; @@ -83,6 +84,36 @@ private async Task MyHybridWebView_OnProxyRequestReceived(HybridWebView.HybridWe { switch (args.QueryParams["operation"]) { + case "fetchWithCors": + if(args.QueryParams.TryGetValue("url", out string urlParam) && !string.IsNullOrWhiteSpace(urlParam)) + { + // Fetch a URL with CORS enabled. +#if ANDROID + var client = new HttpClient(new Xamarin.Android.Net.AndroidMessageHandler()); +#else + var client = new HttpClient(); +#endif + //Enable Cors + client.DefaultRequestHeaders.Add("Access-Control-Allow-Origin", "*"); + + try + { + var response = await client.GetAsync(urlParam); + if (response.IsSuccessStatusCode) + { + args.ResponseStream = await response.Content.ReadAsStreamAsync(); + if (response.Content?.Headers?.ContentType != null) + { + args.ResponseContentType = response.Content.Headers.ContentType.MediaType; + } + } + } + catch (Exception ex) + { + Debug.WriteLine("Error proxying request: " + ex.Message); + } + } + break; case "loadImageFromZip": // Ensure the file name parameter is present. if (args.QueryParams.TryGetValue("fileName", out string fileName) && fileName != null) diff --git a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj index 36cb1ec..4db7c6c 100644 --- a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj +++ b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj @@ -48,6 +48,11 @@ + + + + + @@ -57,7 +62,14 @@ + + + + MSBuild:Compile + + + diff --git a/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html b/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html index d663807..076da0b 100644 --- a/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html +++ b/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html @@ -25,6 +25,34 @@ document.getElementById('proxyImage').src = `${window.location.origin}/proxy?operation=loadImageFromWeb&tileId=${randomInt}`; } + //GitHub does not enable CORS on raw files. This will fail normally in a browser. + var noCorsEnabledFile = 'https://github.com/Eilon/MauiHybridWebView/blob/main/HybridWebView/HybridWebView.cs'; + + function fetchNoCors() { + fetchUrl(noCorsEnabledFile); + } + + function fetchWithCorsEnabledProxy() { + fetchUrl(`${window.location.origin}/proxy?operation=fetchWithCors&url=` + encodeURIComponent(noCorsEnabledFile)); + } + + function fetchUrl(url) { + var outputElm = document.getElementById('fetchResponseTextArea'); + outputElm.value = 'Fetching...'; + + fetch(url).then(response => { + if (!response.ok) { + outputElm.value = 'Error: Network response was not ok'; + } + + return response.text(); + }).then(data => { + outputElm.value = data; + }).catch(error => { + outputElm.value = 'There was a problem with the fetch operation: ' + error; + }); + } + //Load the map. window.onload = function () { var map = new maplibregl.Map({ @@ -76,12 +104,19 @@

HybridWebView demo: Proxy



-

The image below uses an img tag with a proxy URL in it's HTML like <img src="/proxy?operation=loadImageFromZip&fileName=happy.jpeg" /> -
+

+

+ Leverage the proxy to access resources that are on non-CORS enabled domains. +

+ + +
+
+
From 0c09812967360d049d9b5b09d6886a032471a506 Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Fri, 2 Aug 2024 13:55:11 -0700 Subject: [PATCH 2/8] Update MyCustomControl.cs --- ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs index f13b3fe..e5bb8bf 100644 --- a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs +++ b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs @@ -71,7 +71,8 @@ private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxy if (args.QueryParams.TryGetValue("resourceName", out string? resourceName) && !string.IsNullOrWhiteSpace(resourceName)) { var thisAssembly = typeof(MyCustomControl).Assembly; - using (var fs = thisAssembly.GetManifestResourceStream("ExampleWrappedHybridWebViewLibrary." + resourceName.Replace("/", "."))) + var assemblyName = thisAssembly.GetName().Name; + using (var fs = thisAssembly.GetManifestResourceStream($"{assemblyName}.{resourceName.Replace("/", ".")}")) { if (fs != null) { From d5b9497bf230588e348713160cc299ce7e3b4366 Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Fri, 2 Aug 2024 14:50:14 -0700 Subject: [PATCH 3/8] File system resource loading capability. - Add example that leverages the improvements to MainFile loading and a proxy to load web pages and resources from the file system (app data directory). --- .../MyCustomControl.cs | 2 - .../Android/AndroidHybridWebViewClient.cs | 38 ------ .../HybridWebViewHandler.MacCatalyst.cs | 20 +-- .../Windows/HybridWebView.Windows.cs | 21 +--- .../Platforms/iOS/HybridWebViewHandler.iOS.cs | 19 --- MauiCSharpInteropWebView/AppShell.xaml | 4 +- .../LoadFromFileSystemSample.xaml | 13 ++ .../LoadFromFileSystemSample.xaml.cs | 118 ++++++++++++++++++ .../MauiCSharpInteropWebView.csproj | 9 ++ .../Resources/Raw/SampleFileSystemImage.png | Bin 0 -> 1644 bytes .../Resources/Raw/SampleFileSystemPage.html | 45 +++++++ 11 files changed, 189 insertions(+), 100 deletions(-) create mode 100644 MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml create mode 100644 MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs create mode 100644 MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png create mode 100644 MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemPage.html diff --git a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs index e5bb8bf..b6788f6 100644 --- a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs +++ b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs @@ -60,8 +60,6 @@ public MyCustomControl() : base() private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxyEventArgs args) { - // In an app, you might load responses from a sqlite database, zip file, or create files in memory (e.g. using SkiaSharp or System.Drawing) - // Check to see if our custom parameter is present. if (args.QueryParams.ContainsKey("operation")) { diff --git a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs index 8d45344..3003f84 100644 --- a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs +++ b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs @@ -25,45 +25,7 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler) if (!string.IsNullOrEmpty(originalUrl) && new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, webView.MainFile, out string? relativePath, out string contentType, out string fullUrl); - //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); - //IDictionary? customHeaders = null; - - //string contentType; - //if (string.IsNullOrEmpty(relativePath)) - //{ - // //The main file may be a URL that has a query string. Fpor example if we want the main page to go through the proxy. - // if (!string.IsNullOrEmpty(webView.MainFile) && webView.MainFile.Contains("?")) - // { - // relativePath = QueryStringHelper.RemovePossibleQueryString(relativePath); - // } - // else - // { - // relativePath = webView.MainFile; - // } - - // //Try and get the mime type from the full URL (main file could be a URL that has a query string pointing to a file such as a PDF). - // string? minType; - // if(PathUtils.TryGetMimeType(fullUrl, out minType)) - // { - // if (!string.IsNullOrEmpty(minType)) - // { - // contentType = minType; - // } else - // { - // contentType = PathUtils.HtmlMimeType; - // } - // } - // else - // { - // contentType = PathUtils.HtmlMimeType; - // } - //} - //else - //{ - // contentType = PathUtils.GetMimeType(fullUrl); - //} - Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs index f81665c..03efb24 100644 --- a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs +++ b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs @@ -123,25 +123,7 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche { var hwv = (HybridWebView)_webViewHandler.VirtualView; PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); - //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); - - //if (string.IsNullOrEmpty(relativePath)) - //{ - // relativePath = hwv.MainFile!.Replace('\\', '/'); - // contentType = "text/html"; - //} - //else - //{ - // var requestExtension = Path.GetExtension(relativePath); - // contentType = requestExtension switch - // { - // ".htm" or ".html" => "text/html", - // ".js" => "application/javascript", - // ".css" => "text/css", - // _ => "text/plain", - // }; - //} - + Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs index 9aaae68..b5e770c 100644 --- a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs +++ b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs @@ -52,26 +52,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri)) { PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string? relativePath, out string contentType, out string fullUrl); - //var relativePath = AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\'); - - //string contentType; - //if (string.IsNullOrEmpty(relativePath)) - //{ - // relativePath = MainFile; - // contentType = "text/html"; - //} - //else - //{ - // var requestExtension = Path.GetExtension(relativePath); - // contentType = requestExtension switch - // { - // ".htm" or ".html" => "text/html", - // ".js" => "application/javascript", - // ".css" => "text/css", - // _ => "text/plain", - // }; - //} - + Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs index 244dbba..26175ad 100644 --- a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs +++ b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs @@ -125,25 +125,6 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche var hwv = (HybridWebView)_webViewHandler.VirtualView; PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); - //var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/'); - - //if (string.IsNullOrEmpty(relativePath)) - //{ - // relativePath = hwv.MainFile!.Replace('\\', '/'); - // contentType = "text/html"; - //} - //else - //{ - // var requestExtension = Path.GetExtension(relativePath); - // contentType = requestExtension switch - // { - // ".htm" or ".html" => "text/html", - // ".js" => "application/javascript", - // ".css" => "text/css", - // _ => "text/plain", - // }; - //} - Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/MauiCSharpInteropWebView/AppShell.xaml b/MauiCSharpInteropWebView/AppShell.xaml index e6322ec..1a25a9a 100644 --- a/MauiCSharpInteropWebView/AppShell.xaml +++ b/MauiCSharpInteropWebView/AppShell.xaml @@ -7,8 +7,8 @@ Shell.FlyoutBehavior="Flyout"> - - + + diff --git a/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml new file mode 100644 index 0000000..80e1d45 --- /dev/null +++ b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs new file mode 100644 index 0000000..36bd411 --- /dev/null +++ b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs @@ -0,0 +1,118 @@ +using Microsoft.Maui.Storage; +using SQLite; + +namespace MauiCSharpInteropWebView; + +public partial class LoadFromFileSystemSample : ContentPage +{ + public LoadFromFileSystemSample() + { + //For this sample we need a HTML file in the file system. + //We will copy the file from the app package Raw folder to the app data storage. If it isn't already there. + + LoadFileIntoFileSystem("SampleFileSystemPage.html"); + LoadFileIntoFileSystem("SampleFileSystemImage.png"); + + InitializeComponent(); + + //Set the target for JavaScript interop. + myHybridWebView.JSInvokeTarget = new MyJSInvokeTarget(); + + //Monitor proxy requests. + myHybridWebView.ProxyRequestReceived += WebView_ProxyRequestReceived; + } + + #region Private Methods + + private async void LoadFileIntoFileSystem(string assetPath) + { + //Get local file path to app data storage. + var localPath = Path.Combine(FileSystem.AppDataDirectory, Path.GetFileName(assetPath)); + + //Check to see if the file exists in the app data storage already. + if (!File.Exists(localPath)) + { + //If it doesn't, assume it is an asset and copy the file to local app data storage access Raw folder. + using (var asset = await FileSystem.OpenAppPackageFileAsync(assetPath)) + { + using (var file = File.Create(localPath)) + { + asset.CopyTo(file); + } + } + } + } + + private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxyEventArgs args) + { + // Check to see if our custom parameter is present. + if (args.QueryParams.ContainsKey("operation")) + { + switch (args.QueryParams["operation"]) + { + case "loadFromFileSystem": + if (args.QueryParams.TryGetValue("fileName", out string fileName) && !string.IsNullOrWhiteSpace(fileName)) + { + var filePath = System.IO.Path.Combine(FileSystem.Current.AppDataDirectory, Path.GetFileName(fileName)); + + //Check to see if the file exists. + if (File.Exists(filePath)) + { + try + { + using (var fs = System.IO.File.OpenRead(filePath)) + { + var ms = new MemoryStream(); + await fs.CopyToAsync(ms); + ms.Position = 0; + + args.ResponseStream = ms; + } + } + catch (Exception ex) + { + Console.Write(ex.Message); + } + } + } + break; + default: + break; + } + } + } + + private sealed class MyJSInvokeTarget + { + public MyJSInvokeTarget() + { + } + + /// + /// An example of a round trip method that takes an input parameter and returns a simple value type (number). + /// + /// + /// + public double Fibonacci(int n) + { + if (n == 0) return 0; + + int prev = 0; + int next = 1; + for (int i = 1; i < n; i++) + { + int sum = prev + next; + prev = next; + next = sum; + } + return next; + } + } + + #endregion + + private void OnHybridWebViewRawMessageReceived(object sender, HybridWebView.HybridWebViewRawMessageReceivedEventArgs e) + { + + } +} \ No newline at end of file diff --git a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj index 4db7c6c..6ed7b3d 100644 --- a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj +++ b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj @@ -66,10 +66,19 @@ + + + LoadFromFileSystemSample.xaml + + + MSBuild:Compile + + MSBuild:Compile + diff --git a/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png b/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png new file mode 100644 index 0000000000000000000000000000000000000000..dcfdf0b8036828f5c34fa90c77df3e2441d9542f GIT binary patch literal 1644 zcmV-y29x=TP)WFU8GbZ8()Nlj2>E@cM*00q!VL_t(o!_AmYY*bYk zhM#lqoy&CSOqu?7TH68@N-Y#CR#XZq1sKh8@T)5JW3ym)PjXQrfE;O3B zP)IZ}s2hm^L`d_0T2V`0uKO%{|Cb{V7r~U z6{WSMV*Oj|F~*Du;Rp(57O?-L{lYRl4M+lq0P|k~7QiZCt*u>u?W=1kn_q@j8m-4h zVnKqQ_8z)_?Dm0>1^T@O{-2aYF>oJnJrJ3-WEKq%HV~u(q?F^L|7<^NUR^_OaqdV1 z&IdNzjbC$y1@IWK9GDZ1g`KTCw_-5km>nGm@FPC$yV?biW1rOrbX;qOn}MZ34Fb3M z>1N!BOArK?jYSVHB3c?9ssB1)B@n-sfM?iCVm^=;Es3&l^Fo3kb1D2iE?&4u>nE)v zr<4G0w^QSqmBdC1xE2Vte%8vgylE(<{;?cJLMcUPdI;ePI`?$Cc48Rd7;t*B0oMbM z0yhJB3pOoa&b@O`a$;adgO1OP^XraZ35Eh6@Bw9~MtZUU=Kx!PyMRj9cb!#ltith~ z%)pcekF1*s<%WnQVsw1j;n*9{0lFsAfD;A06nMxsN@U5iODJtDMW!#^Xk$Vvw9y=V z{~(3+g(#`U<;Xx%Ft>o-pL-&8QBvd z!JaqwaNykoRNh&MAM-Qj$+RS16Q|?z4z$!^%d!XTwHCrKvb`&Mg}rtez-8qVD}(>hxQ0+&3(fmqt#y{3tV=(j_?4j-LHh>FOMizGO>vk$zDiI-(b!z0-<#YUt;{wP9 zG%#$%(Z2$`-bOtTYI?qja8Wqx%uQcNv-`Q-$U&*BTiy$l>p)Kzt{{JYe%9amc|NJ* zDF(U*JnK-51$=IzH(d>Iw-q|v!wNh6XIPy)vLuj79!bW!zUiX)*5b@(8bHC~ z0`k7fBY8L(2X3?uWuzy50^aeg0AFi&GDbN~ubNK5%>~F|nYGZ*e0_#wdlFlBRaw$B z29ADkl=#9pj-R;_g(Ik1S4G=PZ2%RczA`{QaMlBEvIa6fqkaaFipZZSg-wNMp|g*1 z1H;K-5~~tiMvOZET59?``-#qqVsvJA6swC-^GFSYU4sZmAY4KBu5O`+bty2%vobFV zmyV=(*hU+NvN=M?94n5BsxFt6sJBrHMQQ@36TBLwk5xN qz-}uWbLD_6kpogN^_H|Ef6pJ^tDHU#@pWSW0000 + + + + + + + + + +

HybridWebView demo: Local File system page

+
+ This web page is loaded from the local file system (FileSystem.Current.AppDataDirectory). + It can access resources from in the same way as normal via the HybridAssetRoot and other embedded resources. + Native/JS communication works as expected. The proxy can be further leveraged to reference + local file system files within the web pages (e.g. images). This is useful when you have resources that have + been downloaded and saved to the file system (app data directory). + +

+ +

+ +

+ +

+ The following image is via the local file system use the proxy: +
+
+ +

+
+ + + + From 00c38e2ca1209ba9611f2819094f01f99b83f92d Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Fri, 2 Aug 2024 15:13:31 -0700 Subject: [PATCH 4/8] Update PathUtils.cs --- HybridWebView/PathUtils.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index b275f72..81b1753 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -151,6 +151,10 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m //Sanitize the content type. mimeType = mimeTypeOrFileExtension.ToLowerInvariant() switch { + //WebAssembly file types + "wasm" => "application/wasm", + "wat" => "application/wat", + //Image file types "png" => "image/png", "jpg" or "jpeg" or "jfif" or "pjpeg" or "pjp" => "image/jpeg", @@ -208,6 +212,7 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m "js" or "javascript" => "text/javascript", "css" => "text/css", "csv" => "text/csv", + "md" => "text/markdown", "plain" or "txt" => "text/plain", _ => null, From 82130b723f12201e8da5d30120c918a5c78e0bd4 Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Fri, 2 Aug 2024 15:18:10 -0700 Subject: [PATCH 5/8] Update PathUtils.cs --- HybridWebView/PathUtils.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index 81b1753..11500e8 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -153,7 +153,6 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m { //WebAssembly file types "wasm" => "application/wasm", - "wat" => "application/wat", //Image file types "png" => "image/png", @@ -213,7 +212,7 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m "css" => "text/css", "csv" => "text/csv", "md" => "text/markdown", - "plain" or "txt" => "text/plain", + "plain" or "txt" or "wat" => "text/plain", _ => null, }; From b9c138dd65149b959805bdb789c1a8da2c3d6a0b Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Thu, 8 Aug 2024 03:15:05 -0700 Subject: [PATCH 6/8] Minor fixes --- HybridWebView/PathUtils.cs | 2 +- HybridWebView/Platforms/Windows/HybridWebView.Windows.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index 11500e8..04b2c19 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -158,7 +158,7 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m "png" => "image/png", "jpg" or "jpeg" or "jfif" or "pjpeg" or "pjp" => "image/jpeg", "gif" => "image/gif", - "webp" => "image/wemp", + "webp" => "image/webp", "svg" or "svg+xml" => "image/svg+xml", "ico" or "x-icon" => "image/x-icon", "bmp" => "image/bmp", diff --git a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs index b5e770c..c5eb29c 100644 --- a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs +++ b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs @@ -46,9 +46,9 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe { // 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 requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri); - + if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri)) { PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string? relativePath, out string contentType, out string fullUrl); From 7967e0d3b4de0c14e736e8d25c4639b6b6366a9d Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Thu, 8 Aug 2024 07:45:24 -0700 Subject: [PATCH 7/8] Minor fixes --- HybridWebView/PathUtils.cs | 2 +- .../Platforms/Android/AndroidHybridWebViewClient.cs | 2 +- .../MacCatalyst/HybridWebViewHandler.MacCatalyst.cs | 6 +++--- HybridWebView/Platforms/Windows/HybridWebView.Windows.cs | 2 +- HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index 04b2c19..d42c2b3 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -12,7 +12,7 @@ public static string NormalizePath(string filename) => .Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar); - public static void GetRelativePathAndContentType(Uri appOriginUri, Uri requestUri, string originalUrl, string? mainFileName, out string? relativePath, out string contentType, out string fullUrl) + public static void GetRelativePathAndContentType(Uri appOriginUri, Uri requestUri, string originalUrl, string? mainFileName, out string relativePath, out string contentType, out string fullUrl) { relativePath = appOriginUri.MakeRelativeUri(requestUri).ToString().Replace('/', '\\'); fullUrl = originalUrl; diff --git a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs index 3003f84..35592af 100644 --- a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs +++ b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs @@ -24,7 +24,7 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler) if (!string.IsNullOrEmpty(originalUrl) && new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { - PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, webView.MainFile, out string? relativePath, out string contentType, out string fullUrl); + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, webView.MainFile, out string relativePath, out string contentType, out string fullUrl); Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs index 03efb24..b7a38a6 100644 --- a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs +++ b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs @@ -122,7 +122,7 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { var hwv = (HybridWebView)_webViewHandler.VirtualView; - PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string relativePath, out string contentType, out string fullUrl); Stream? contentStream = null; IDictionary? customHeaders = null; @@ -153,8 +153,8 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders); } - var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); - var assetPath = Path.Combine(bundleRootDir, relativePath); + string bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot ?? ""); + string assetPath = Path.Combine(bundleRootDir, relativePath); if (File.Exists(assetPath)) { diff --git a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs index c5eb29c..0b2c70b 100644 --- a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs +++ b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs @@ -51,7 +51,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri)) { - PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string? relativePath, out string contentType, out string fullUrl); + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string relativePath, out string contentType, out string fullUrl); Stream? contentStream = null; IDictionary? customHeaders = null; diff --git a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs index 26175ad..5dd342a 100644 --- a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs +++ b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs @@ -123,7 +123,7 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri)) { var hwv = (HybridWebView)_webViewHandler.VirtualView; - PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string? relativePath, out string contentType, out string fullUrl); + PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string relativePath, out string contentType, out string fullUrl); Stream? contentStream = null; IDictionary? customHeaders = null; @@ -155,8 +155,8 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders); } - var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!); - var assetPath = Path.Combine(bundleRootDir, relativePath); + string bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot ?? ""); + string assetPath = Path.Combine(bundleRootDir, relativePath); if (File.Exists(assetPath)) { From 331c92cb7b1a090e25631a05005e7c14aa6bbdd3 Mon Sep 17 00:00:00 2001 From: Ricky Brundritt Date: Thu, 8 Aug 2024 08:52:03 -0700 Subject: [PATCH 8/8] Update PathUtils.cs --- HybridWebView/PathUtils.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs index d42c2b3..d137e13 100644 --- a/HybridWebView/PathUtils.cs +++ b/HybridWebView/PathUtils.cs @@ -198,6 +198,7 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m //Other binary file types "zip" => "application/zip", "pbf" or "x-protobuf" => "application/x-protobuf", + "mvt" or "vnd.mapbox-vector-tile" => "application/vnd.mapbox-vector-tile", "kmz" or "vnd.google-earth.kmz" or "shp" or "dbf" or "bin" or "b3dm" or "i3dm" or "pnts" or "subtree" or "octet-stream" => "application/octet-stream", "pdf" => "application/pdf", @@ -207,7 +208,7 @@ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? m //Text based file types "htm" or "html" => "text/html", - "xhtml" => "application/xhtml+xml", + "xhtml" or "xhtml+xml" => "application/xhtml+xml", "js" or "javascript" => "text/javascript", "css" => "text/css", "csv" => "text/csv",