diff --git a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs b/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs index 5c634ab208..e63fa411fc 100644 --- a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs +++ b/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs @@ -2,6 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; @@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot) { - BasePath = new PathString(pathPrefix.StartsWith("/") ? pathPrefix : "/" + pathPrefix); + BasePath = NormalizePath(pathPrefix); InnerProvider = new PhysicalFileProvider(contentRoot); } @@ -40,20 +44,30 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets /// public IDirectoryContents GetDirectoryContents(string subpath) { - if (!StartsWithBasePath(subpath, out var physicalPath)) - { - return NotFoundDirectoryContents.Singleton; - } - else + var modifiedSub = NormalizePath(subpath); + + if (StartsWithBasePath(modifiedSub, out var physicalPath)) { return InnerProvider.GetDirectoryContents(physicalPath.Value); } + else if (string.Equals(subpath, string.Empty) || string.Equals(modifiedSub, "/")) + { + return new StaticWebAssetsDirectoryRoot(BasePath); + } + else if (BasePath.StartsWithSegments(modifiedSub, FilePathComparison, out var remaining)) + { + return new StaticWebAssetsDirectoryRoot(remaining); + } + + return NotFoundDirectoryContents.Singleton; } /// public IFileInfo GetFileInfo(string subpath) { - if (!StartsWithBasePath(subpath, out var physicalPath)) + var modifiedSub = NormalizePath(subpath); + + if (!StartsWithBasePath(modifiedSub, out var physicalPath)) { return new NotFoundFileInfo(subpath); } @@ -69,9 +83,69 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets return InnerProvider.Watch(filter); } + private static string NormalizePath(string path) + { + path = path.Replace('\\', '/'); + return path != null && path.StartsWith("/") ? path : "/" + path; + } + private bool StartsWithBasePath(string subpath, out PathString rest) { return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest); } + + private class StaticWebAssetsDirectoryRoot : IDirectoryContents + { + private readonly string _nextSegment; + + public StaticWebAssetsDirectoryRoot(PathString remainingPath) + { + // We MUST use the Value property here because it is unescaped. + _nextSegment = remainingPath.Value.Split("/", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + } + + public bool Exists => true; + + public IEnumerator GetEnumerator() + { + return GenerateEnum(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GenerateEnum(); + } + + private IEnumerator GenerateEnum() + { + return new[] { new StaticWebAssetsFileInfo(_nextSegment) } + .Cast().GetEnumerator(); + } + + private class StaticWebAssetsFileInfo : IFileInfo + { + public StaticWebAssetsFileInfo(string name) + { + Name = name; + } + + public bool Exists => true; + + public long Length => throw new NotImplementedException(); + + public string PhysicalPath => throw new NotImplementedException(); + + public DateTimeOffset LastModified => throw new NotImplementedException(); + + public bool IsDirectory => true; + + public string Name { get; } + + public Stream CreateReadStream() + { + throw new NotImplementedException(); + } + } + } } } diff --git a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj index 7c9b9141a1..62619a73b2 100644 --- a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj +++ b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -9,7 +9,6 @@ - diff --git a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs index 249f0a1670..5fc2800e0f 100644 --- a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs +++ b/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs @@ -37,6 +37,75 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets Assert.Equal("/_content", provider.BasePath); } + [Theory] + [InlineData("\\", "_content")] + [InlineData("\\_content\\RazorClassLib\\Dir", "Castle.Core.dll")] + [InlineData("", "_content")] + [InlineData("/", "_content")] + [InlineData("/_content", "RazorClassLib")] + [InlineData("/_content/RazorClassLib", "Dir")] + [InlineData("/_content/RazorClassLib/Dir", "Microsoft.AspNetCore.Hosting.Tests.dll")] + [InlineData("/_content/RazorClassLib/Dir/testroot/", "TextFile.txt")] + [InlineData("/_content/RazorClassLib/Dir/testroot/wwwroot/", "README")] + public void GetDirectoryContents_WalksUpContentRoot(string searchDir, string expected) + { + // Arrange + var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/Dir", AppContext.BaseDirectory); + + // Act + var directory = provider.GetDirectoryContents(searchDir); + + // Assert + Assert.NotEmpty(directory); + Assert.Contains(directory, file => string.Equals(file.Name, expected)); + } + + [Fact] + public void GetDirectoryContents_DoesNotFindNonExistentFiles() + { + // Arrange + var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/", AppContext.BaseDirectory); + + // Act + var directory = provider.GetDirectoryContents("/_content/RazorClassLib/False"); + + // Assert + Assert.Empty(directory); + } + + [Theory] + [InlineData("/False/_content/RazorClassLib/")] + [InlineData("/_content/RazorClass")] + public void GetDirectoryContents_PartialMatchFails(string requestedUrl) + { + // Arrange + var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib", AppContext.BaseDirectory); + + // Act + var directory = provider.GetDirectoryContents(requestedUrl); + + // Assert + Assert.Empty(directory); + } + + [Fact] + public void GetDirectoryContents_HandlesWhitespaceInBase() + { + // Arrange + var provider = new StaticWebAssetsFileProvider("/_content/Static Web Assets", + Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); + + // Act + var directory = provider.GetDirectoryContents("/_content/Static Web Assets/Static Web/"); + + // Assert + Assert.Collection(directory, + file => + { + Assert.Equal("Static Web.txt", file.Name); + }); + } + [Fact] public void StaticWebAssetsFileProvider_FindsFileWithSpaces() { diff --git a/src/Hosting/Hosting/test/testroot/wwwroot/Static Web/Static Web.txt b/src/Hosting/Hosting/test/testroot/wwwroot/Static Web/Static Web.txt new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/Hosting/Hosting/test/testroot/wwwroot/Static Web/Static Web.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index 38b8d99c54..70385d88d5 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Mvc/test/Mvc.FunctionalTests/TagHelpersTest.cs b/src/Mvc/test/Mvc.FunctionalTests/TagHelpersTest.cs index e6559100f4..9d2daf2f59 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/TagHelpersTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/TagHelpersTest.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient EncodedClient { get; } [Theory] + [InlineData("GlobbingTagHelpers")] [InlineData("Index")] [InlineData("About")] [InlineData("Help")] diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.GlobbingTagHelpers.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.GlobbingTagHelpers.html new file mode 100644 index 0000000000..6f38146bb3 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.GlobbingTagHelpers.html @@ -0,0 +1,39 @@ + + + + + + + + Globbing Page - My ASP.NET Application + + + + +

ASP.NET vNext - Globbing Page

+

| My Home + | My About + | My Help |

+
+ + +Globbing + +
+
+ + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorPagesClassLibrary/RazorPagesClassLibrary.csproj b/src/Mvc/test/WebSites/RazorPagesClassLibrary/RazorPagesClassLibrary.csproj index 252fd7e3e2..ff47b923a8 100644 --- a/src/Mvc/test/WebSites/RazorPagesClassLibrary/RazorPagesClassLibrary.csproj +++ b/src/Mvc/test/WebSites/RazorPagesClassLibrary/RazorPagesClassLibrary.csproj @@ -5,7 +5,7 @@ true true
- + diff --git a/src/Mvc/test/WebSites/RazorPagesClassLibrary/wwwroot/razorlib/file.js b/src/Mvc/test/WebSites/RazorPagesClassLibrary/wwwroot/razorlib/file.js new file mode 100644 index 0000000000..1a422a7ef0 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesClassLibrary/wwwroot/razorlib/file.js @@ -0,0 +1,5 @@ +window.exampleJsFunctions = { + showPrompt: function (message) { + return prompt(message, 'Type anything here') + } +}; \ No newline at end of file diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs b/src/Mvc/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs index 011efff659..f77e3dc265 100644 --- a/src/Mvc/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs @@ -25,6 +25,11 @@ namespace TagHelpersWebSite.Controllers return View(); } + public IActionResult GlobbingTagHelpers() + { + return View(); + } + public IActionResult Help() { return View(); diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs b/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs index f7caea4035..86313a87c1 100644 --- a/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs @@ -21,6 +21,7 @@ namespace TagHelpersWebSite public void Configure(IApplicationBuilder app) { app.UseRouting(); + app.UseStaticFiles(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); @@ -38,6 +39,7 @@ namespace TagHelpersWebSite public static IWebHostBuilder CreateWebHostBuilder(string[] args) => new WebHostBuilder() .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStaticWebAssets() .UseStartup() .UseKestrel() .UseIISIntegration(); diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/TagHelpersWebSite.csproj b/src/Mvc/test/WebSites/TagHelpersWebSite/TagHelpersWebSite.csproj index 38cfcb61aa..cee74e7d23 100644 --- a/src/Mvc/test/WebSites/TagHelpersWebSite/TagHelpersWebSite.csproj +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/TagHelpersWebSite.csproj @@ -6,6 +6,10 @@ true + + + + diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/GlobbingTagHelpers.cshtml b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/GlobbingTagHelpers.cshtml new file mode 100644 index 0000000000..421cffce3f --- /dev/null +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/GlobbingTagHelpers.cshtml @@ -0,0 +1,19 @@ +@{ + ViewData["Title"] = "Globbing Page"; +} + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +@section footerContent { + + + + + + + + + + +} +Globbing diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/wwwroot/js/dist/dashboardHome.js b/src/Mvc/test/WebSites/TagHelpersWebSite/wwwroot/js/dist/dashboardHome.js new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/wwwroot/js/dist/dashboardHome.js @@ -0,0 +1 @@ + \ No newline at end of file