From e3eb2e0d62aa448ce2ac791110c5c0dc204f1856 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Thu, 25 Mar 2021 11:49:30 -0700 Subject: [PATCH 1/3] Fix WebView header APIs and behavior - Change WebView to return dictionary of headers instead of strings (this is needed for Android) - Change StaticContentProvider to use filename (always exists) instead of physical file path (not always available) to determine MIME type - Fix WPF sample bug --- .../WebView2/src/WebView2WebViewManager.cs | 8 +- .../Samples/BlazorWpfApp/Pages/Index.razor | 2 +- .../WebView/src/PublicAPI.Unshipped.txt | 2 +- .../WebView/src/StaticContentProvider.cs | 13 +- .../WebView/WebView/src/WebViewManager.cs | 2 +- .../test/StaticContentProviderTests.cs | 140 ++++++++++++++++++ 6 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 src/Components/WebView/WebView/test/StaticContentProviderTests.cs diff --git a/src/Components/WebView/Platforms/WebView2/src/WebView2WebViewManager.cs b/src/Components/WebView/Platforms/WebView2/src/WebView2WebViewManager.cs index a26c13d49ed8..3180af07cdaf 100644 --- a/src/Components/WebView/Platforms/WebView2/src/WebView2WebViewManager.cs +++ b/src/Components/WebView/Platforms/WebView2/src/WebView2WebViewManager.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.FileProviders; using Microsoft.Web.WebView2.Core; @@ -73,7 +75,8 @@ private async Task InitializeWebView2() if (TryGetResponseContent(eventArgs.Request.Uri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)) { - eventArgs.Response = environment.CreateWebResourceResponse(content, statusCode, statusMessage, headers); + var headerString = GetHeaderString(headers); + eventArgs.Response = environment.CreateWebResourceResponse(content, statusCode, statusMessage, headerString); } }; @@ -94,6 +97,9 @@ await _webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@" => MessageReceived(new Uri(eventArgs.Source), eventArgs.TryGetWebMessageAsString()); } + private static string GetHeaderString(IDictionary headers) => + string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}")); + private void ApplyDefaultWebViewSettings() { // Desktop applications typically don't want the default web browser context menu diff --git a/src/Components/WebView/Samples/BlazorWpfApp/Pages/Index.razor b/src/Components/WebView/Samples/BlazorWpfApp/Pages/Index.razor index e8ceefc23d93..499d798b9bd4 100644 --- a/src/Components/WebView/Samples/BlazorWpfApp/Pages/Index.razor +++ b/src/Components/WebView/Samples/BlazorWpfApp/Pages/Index.razor @@ -1,6 +1,6 @@ @page "/" @using WebviewAppShared -// NOTE: The full namespace is included here to work around this bug: https://github.com/dotnet/aspnetcore/issues/30851 +@* NOTE: The full namespace is included here to work around this bug: https://github.com/dotnet/aspnetcore/issues/30851 *@ @inject BlazorWpfApp.AppState AppState

Hello, world!

diff --git a/src/Components/WebView/WebView/src/PublicAPI.Unshipped.txt b/src/Components/WebView/WebView/src/PublicAPI.Unshipped.txt index 4d456858d373..10beab9cbe48 100644 --- a/src/Components/WebView/WebView/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebView/WebView/src/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ Microsoft.AspNetCore.Components.WebView.WebViewManager.Dispose() -> void Microsoft.AspNetCore.Components.WebView.WebViewManager.MessageReceived(System.Uri! sourceUri, string! message) -> void Microsoft.AspNetCore.Components.WebView.WebViewManager.Navigate(string! url) -> void Microsoft.AspNetCore.Components.WebView.WebViewManager.RemoveRootComponentAsync(string! selector) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.WebView.WebViewManager.TryGetResponseContent(string! uri, bool allowFallbackOnHostPage, out int statusCode, out string! statusMessage, out System.IO.Stream! content, out string! headers) -> bool +Microsoft.AspNetCore.Components.WebView.WebViewManager.TryGetResponseContent(string! uri, bool allowFallbackOnHostPage, out int statusCode, out string! statusMessage, out System.IO.Stream! content, out System.Collections.Generic.IDictionary! headers) -> bool Microsoft.AspNetCore.Components.WebView.WebViewManager.WebViewManager(System.IServiceProvider! provider, Microsoft.AspNetCore.Components.Dispatcher! dispatcher, System.Uri! appBaseUri, Microsoft.Extensions.FileProviders.IFileProvider! fileProvider, string! hostPageRelativePath) -> void Microsoft.Extensions.DependencyInjection.ComponentsWebViewServiceCollectionExtensions abstract Microsoft.AspNetCore.Components.WebView.WebViewManager.NavigateCore(System.Uri! absoluteUri) -> void diff --git a/src/Components/WebView/WebView/src/StaticContentProvider.cs b/src/Components/WebView/WebView/src/StaticContentProvider.cs index a8e700e356e0..3c6276531d12 100644 --- a/src/Components/WebView/WebView/src/StaticContentProvider.cs +++ b/src/Components/WebView/WebView/src/StaticContentProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.Text; using Microsoft.Extensions.FileProviders; @@ -24,7 +25,7 @@ public StaticContentProvider(IFileProvider fileProvider, Uri appBaseUri, string _hostPageRelativePath = hostPageRelativePath ?? throw new ArgumentNullException(nameof(hostPageRelativePath)); } - public bool TryGetResponseContent(string requestUri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out string headers) + public bool TryGetResponseContent(string requestUri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary headers) { var fileUri = new Uri(requestUri); if (_appBaseUri.IsBaseOf(fileUri)) @@ -75,7 +76,7 @@ private bool TryGetFromFileProvider(string relativePath, out Stream content, out if (fileInfo.Exists) { content = fileInfo.CreateReadStream(); - contentType = GetResponseContentTypeOrDefault(fileInfo.PhysicalPath); + contentType = GetResponseContentTypeOrDefault(fileInfo.Name); return true; } } @@ -107,7 +108,11 @@ private static string GetResponseContentTypeOrDefault(string path) ? matchedContentType : "application/octet-stream"; - private static string GetResponseHeaders(string contentType) - => $"Content-Type: {contentType}{Environment.NewLine}Cache-Control: no-cache, max-age=0, must-revalidate, no-store"; + private static IDictionary GetResponseHeaders(string contentType) + => new Dictionary() + { + {"Content-Type", contentType}, + { "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" }, + }; } } diff --git a/src/Components/WebView/WebView/src/WebViewManager.cs b/src/Components/WebView/WebView/src/WebViewManager.cs index ed72ebfa35ef..0188d12a3167 100644 --- a/src/Components/WebView/WebView/src/WebViewManager.cs +++ b/src/Components/WebView/WebView/src/WebViewManager.cs @@ -168,7 +168,7 @@ protected void MessageReceived(Uri sourceUri, string message) /// The response content /// The response headers /// true if the response can be provided; false otherwise. - protected bool TryGetResponseContent(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out string headers) + protected bool TryGetResponseContent(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary headers) => _staticContentProvider.TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers); internal async Task AttachToPageAsync(string baseUrl, string startUrl) diff --git a/src/Components/WebView/WebView/test/StaticContentProviderTests.cs b/src/Components/WebView/WebView/test/StaticContentProviderTests.cs new file mode 100644 index 000000000000..fe961eddb3af --- /dev/null +++ b/src/Components/WebView/WebView/test/StaticContentProviderTests.cs @@ -0,0 +1,140 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Components.WebView +{ + public class StaticContentProviderTests + { + [Fact] + public void TryGetResponseContentReturnsCorrectContentTypeForNonPhysicalFile() + { + // Arrange + const string cssFilePath = "folder/file.css"; + const string cssFileContent = "this is css"; + var inMemoryFileProvider = new InMemoryFileProvider( + new Dictionary + { + { cssFilePath, cssFileContent }, + }); + var appBase = "fake://0.0.0.0/"; + var scp = new StaticContentProvider(inMemoryFileProvider, new Uri(appBase), "fakehost.html"); + + // Act + Assert.True(scp.TryGetResponseContent( + requestUri: appBase + "folder/file.css", + allowFallbackOnHostPage: false, + out var statusCode, + out var statusMessage, + out var content, + out var headers)); + + // Assert + var contentString = new StreamReader(content).ReadToEnd(); + Assert.Equal(200, statusCode); + Assert.Equal("OK", statusMessage); + Assert.Equal("this is css", contentString); + Assert.True(headers.TryGetValue("Content-Type", out var contentTypeValue)); + Assert.Equal("text/css", contentTypeValue); + } + + private sealed class InMemoryFileProvider : IFileProvider + { + public InMemoryFileProvider(IDictionary filePathsAndContents) + { + if (filePathsAndContents is null) + { + throw new ArgumentNullException(nameof(filePathsAndContents)); + } + + FilePathsAndContents = filePathsAndContents; + } + + public IDictionary FilePathsAndContents { get; } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + return new InMemoryDirectoryContents(this, subpath); + } + + public IFileInfo GetFileInfo(string subpath) + { + return FilePathsAndContents + .Where(kvp => kvp.Key == subpath) + .Select(x => new InMemoryFileInfo(x.Key, x.Value)) + .Single(); + } + + public IChangeToken Watch(string filter) + { + return null; + } + + private sealed class InMemoryDirectoryContents : IDirectoryContents + { + private readonly InMemoryFileProvider _inMemoryFileProvider; + private readonly string _subPath; + + public InMemoryDirectoryContents(InMemoryFileProvider inMemoryFileProvider, string subPath) + { + _inMemoryFileProvider = inMemoryFileProvider ?? throw new ArgumentNullException(nameof(inMemoryFileProvider)); + _subPath = subPath ?? throw new ArgumentNullException(nameof(inMemoryFileProvider)); + } + + public bool Exists => true; + + public IEnumerator GetEnumerator() + { + return + _inMemoryFileProvider + .FilePathsAndContents + .Where(kvp => kvp.Key.StartsWith(_subPath, StringComparison.Ordinal)) + .Select(x => new InMemoryFileInfo(x.Key, x.Value)) + .GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private sealed class InMemoryFileInfo : IFileInfo + { + private readonly string _filePath; + private readonly string _fileContents; + + public InMemoryFileInfo(string filePath, string fileContents) + { + _filePath = filePath; + _fileContents = fileContents; + } + + public bool Exists => true; + + public long Length => _fileContents.Length; + + public string PhysicalPath => null; + + public string Name => Path.GetFileName(_filePath); + + public DateTimeOffset LastModified => DateTimeOffset.Now; + + public bool IsDirectory => false; + + public Stream CreateReadStream() + { + return new MemoryStream(Encoding.UTF8.GetBytes(_fileContents)); + } + } + } + } +} From 5719cd0654381822ea48a7ac467b8b42ab6ca577 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Thu, 25 Mar 2021 11:51:53 -0700 Subject: [PATCH 2/3] Code format --- src/Components/WebView/WebView/src/StaticContentProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/WebView/WebView/src/StaticContentProvider.cs b/src/Components/WebView/WebView/src/StaticContentProvider.cs index 3c6276531d12..d1df73709379 100644 --- a/src/Components/WebView/WebView/src/StaticContentProvider.cs +++ b/src/Components/WebView/WebView/src/StaticContentProvider.cs @@ -111,7 +111,7 @@ private static string GetResponseContentTypeOrDefault(string path) private static IDictionary GetResponseHeaders(string contentType) => new Dictionary() { - {"Content-Type", contentType}, + { "Content-Type", contentType }, { "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" }, }; } From a005af53fc627403e21d9bf2e0bf3bd3961388c0 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Thu, 25 Mar 2021 11:52:41 -0700 Subject: [PATCH 3/3] Use const --- .../WebView/WebView/test/StaticContentProviderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/WebView/WebView/test/StaticContentProviderTests.cs b/src/Components/WebView/WebView/test/StaticContentProviderTests.cs index fe961eddb3af..dcfc14063c5d 100644 --- a/src/Components/WebView/WebView/test/StaticContentProviderTests.cs +++ b/src/Components/WebView/WebView/test/StaticContentProviderTests.cs @@ -30,7 +30,7 @@ public void TryGetResponseContentReturnsCorrectContentTypeForNonPhysicalFile() // Act Assert.True(scp.TryGetResponseContent( - requestUri: appBase + "folder/file.css", + requestUri: appBase + cssFilePath, allowFallbackOnHostPage: false, out var statusCode, out var statusMessage,