[Fixes #2841] Support comma separated globbed include and exclude pattern in Script and Link tag helpers

This commit is contained in:
Ajay Bhargav Baaskaran 2015-08-04 15:52:23 -07:00
parent ed4896d1fd
commit fcad4c5c57
10 changed files with 100 additions and 50 deletions

View File

@ -20,6 +20,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
private static readonly char[] PatternSeparator = new[] { ',' }; private static readonly char[] PatternSeparator = new[] { ',' };
// Valid whitespace characters defined by the HTML5 spec.
private static readonly char[] ValidAttributeWhitespaceChars =
new[] { '\t', '\n', '\u000C', '\r', ' ' };
private static readonly PathComparer DefaultPathComparer = new PathComparer(); private static readonly PathComparer DefaultPathComparer = new PathComparer();
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory; private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
@ -125,11 +129,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher(); var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher();
matcher.AddIncludePatterns(includePatterns.Select(pattern => TrimLeadingSlash(pattern))); matcher.AddIncludePatterns(includePatterns.Select(pattern => TrimLeadingTildeSlash(pattern)));
if (excludePatterns != null) if (excludePatterns != null)
{ {
matcher.AddExcludePatterns(excludePatterns.Select(pattern => TrimLeadingSlash(pattern))); matcher.AddExcludePatterns(excludePatterns.Select(pattern => TrimLeadingTildeSlash(pattern)));
} }
var matches = matcher.Execute(_baseGlobbingDirectory); var matches = matcher.Execute(_baseGlobbingDirectory);
@ -210,11 +214,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
} }
} }
private static string TrimLeadingSlash(string value) private static string TrimLeadingTildeSlash(string value)
{ {
var result = value; var result = value.Trim(ValidAttributeWhitespaceChars);
if (result.StartsWith("/", StringComparison.Ordinal) || if (result.StartsWith("~/", StringComparison.Ordinal))
{
result = result.Substring(2);
}
else if (result.StartsWith("/", StringComparison.Ordinal) ||
result.StartsWith("\\", StringComparison.Ordinal)) result.StartsWith("\\", StringComparison.Ordinal))
{ {
// Trim the leading slash as the matcher runs from the provided root only anyway // Trim the leading slash as the matcher runs from the provided root only anyway

View File

@ -253,15 +253,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (mode == Mode.GlobbedHref || mode == Mode.Fallback && !string.IsNullOrEmpty(HrefInclude)) if (mode == Mode.GlobbedHref || mode == Mode.Fallback && !string.IsNullOrEmpty(HrefInclude))
{ {
if (TryResolveUrl(HrefInclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
HrefInclude = resolvedUrl;
}
if (TryResolveUrl(HrefExclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
HrefExclude = resolvedUrl;
}
BuildGlobbedLinkTags(attributes, builder); BuildGlobbedLinkTags(attributes, builder);
if (string.IsNullOrEmpty(Href)) if (string.IsNullOrEmpty(Href))
{ {
@ -277,14 +268,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
FallbackHref = resolvedUrl; FallbackHref = resolvedUrl;
} }
if (TryResolveUrl(FallbackHrefInclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
FallbackHrefInclude = resolvedUrl;
}
if (TryResolveUrl(FallbackHrefExclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
FallbackHrefExclude = resolvedUrl;
}
BuildFallbackBlock(builder); BuildFallbackBlock(builder);
} }

View File

@ -220,15 +220,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (mode == Mode.GlobbedSrc || mode == Mode.Fallback && !string.IsNullOrEmpty(SrcInclude)) if (mode == Mode.GlobbedSrc || mode == Mode.Fallback && !string.IsNullOrEmpty(SrcInclude))
{ {
if (TryResolveUrl(SrcInclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
SrcInclude = resolvedUrl;
}
if (TryResolveUrl(SrcExclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
SrcExclude = resolvedUrl;
}
BuildGlobbedScriptTags(attributes, builder); BuildGlobbedScriptTags(attributes, builder);
if (string.IsNullOrEmpty(Src)) if (string.IsNullOrEmpty(Src))
{ {
@ -244,14 +235,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
FallbackSrc = resolvedUrl; FallbackSrc = resolvedUrl;
} }
if (TryResolveUrl(FallbackSrcInclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
FallbackSrcInclude = resolvedUrl;
}
if (TryResolveUrl(FallbackSrcExclude, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
FallbackSrcExclude = resolvedUrl;
}
BuildFallbackBlock(attributes, builder); BuildFallbackBlock(attributes, builder);
} }

View File

@ -10,6 +10,9 @@
<!-- Globbed link tag with existing file --> <!-- Globbed link tag with existing file -->
<link rel="stylesheet" title="&lt;the title>" href="HtmlEncode[[/site.css]]" /> <link rel="stylesheet" title="&lt;the title>" href="HtmlEncode[[/site.css]]" />
<!-- Globbed link tag with comma separated pattern -->
<link rel="stylesheet" title="&lt;the title" href="HtmlEncode[[/site.css]]" /><link rel="stylesheet" title="&lt;the title" href="HtmlEncode[[/sub/site2.css]]" /><link rel="stylesheet" title="&lt;the title" href="HtmlEncode[[/sub/site3.css]]" /><link rel="stylesheet" title="&lt;the title" href="HtmlEncode[[/sub/site3.min.css]]" />
<!-- Globbed link tag with existing file and exclude --> <!-- Globbed link tag with existing file and exclude -->
<link rel="stylesheet" title="&quot;the&quot; title" href="HtmlEncode[[/site.css]]" /><link rel="stylesheet" title="&quot;the&quot; title" href="HtmlEncode[[/sub/site2.css]]" /> <link rel="stylesheet" title="&quot;the&quot; title" href="HtmlEncode[[/site.css]]" /><link rel="stylesheet" title="&quot;the&quot; title" href="HtmlEncode[[/sub/site2.css]]" />

View File

@ -10,6 +10,9 @@
<!-- Globbed link tag with existing file --> <!-- Globbed link tag with existing file -->
<link rel="stylesheet" title="&lt;the title>" href="/site.css" /> <link rel="stylesheet" title="&lt;the title>" href="/site.css" />
<!-- Globbed link tag with comma separated pattern -->
<link rel="stylesheet" title="&lt;the title" href="/site.css" /><link rel="stylesheet" title="&lt;the title" href="/sub/site2.css" /><link rel="stylesheet" title="&lt;the title" href="/sub/site3.css" /><link rel="stylesheet" title="&lt;the title" href="/sub/site3.min.css" />
<!-- Globbed link tag with existing file and exclude --> <!-- Globbed link tag with existing file and exclude -->
<link rel="stylesheet" title="&quot;the&quot; title" href="/site.css" /><link rel="stylesheet" title="&quot;the&quot; title" href="/sub/site2.css" /> <link rel="stylesheet" title="&quot;the&quot; title" href="/site.css" /><link rel="stylesheet" title="&quot;the&quot; title" href="/sub/site2.css" />

View File

@ -75,6 +75,9 @@
// Globbed script tag missing include but with static src // Globbed script tag missing include but with static src
</script> </script>
<!-- Globbed script tag with comma separated include pattern -->
<script src="HtmlEncode[[/site.js]]"></script><script src="HtmlEncode[[/sub/site2.js]]"></script><script src="HtmlEncode[[/sub/site3.js]]"></script>
<!-- Globbed script tag with missing file --> <!-- Globbed script tag with missing file -->

View File

@ -75,6 +75,9 @@
// Globbed script tag missing include but with static src // Globbed script tag missing include but with static src
</script> </script>
<!-- Globbed script tag with comma separated include pattern -->
<script src="/site.js"></script><script src="/sub/site2.js"></script><script src="/sub/site3.js"></script>
<!-- Globbed script tag with missing file --> <!-- Globbed script tag with missing file -->

View File

@ -86,7 +86,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
/* staticUrl */ "/site.css", /* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] { /* dirStructure */ new FileNode(null, new [] {
new FileNode("A", new [] { new FileNode("A", new [] {
new FileNode("c.css"), new FileNode("c.css"),
new FileNode("d.css") new FileNode("d.css")
@ -299,10 +298,68 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
Mock.Get(cache).VerifyAll(); Mock.Get(cache).VerifyAll();
} }
public static TheoryData CommaSeparatedPatternData
{
get
{
// Include pattern, expected output
return new TheoryData<string, string[]>
{
{
"~/*.css, ~/*.txt",
new[] { "/site.css", "/site2.txt" }
},
{
"*.css, /*.txt",
new[] { "/site.css", "/site2.txt" }
},
{
"\\*.css,~/*.txt",
new[] { "/site.css", "/site2.txt" }
},
{
"~/*.js, *.txt",
new[] { "/blank.js", "/site.js", "/site2.txt" }
},
{
" ~/*.js,*.txt, /*.css",
new[] { "/blank.js", "/site.css", "/site.js", "/site2.txt" }
},
{
"~/blank.js, blank.js,/blank.js, \\blank.js",
new[] { "/blank.js" }
},
};
}
}
[Theory] [Theory]
[MemberData(nameof(CommaSeparatedPatternData))]
public void HandlesCommaSeparatedPatterns(string includePattern, string[] expectedOutput)
{
// Arrange
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.js", "site2.txt", "site.js"));
IMemoryCache cache = null;
var requestPathBase = PathString.Empty;
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
// Act
var urlList = globbingUrlBuilder.BuildUrlList(
staticUrl: null,
includePattern: includePattern,
excludePattern: null);
// Assert
Assert.Equal(expectedOutput, urlList, StringComparer.Ordinal);
}
[Theory]
[InlineData("")]
[InlineData("/")] [InlineData("/")]
[InlineData("\\")] [InlineData(" \\")]
public void TrimsLeadingSlashFromPatterns(string leadingSlash) [InlineData("~/")]
[InlineData(" ~/")]
public void TrimsLeadingTildeAndSlashFromPatterns(string prefix)
{ {
// Arrange // Arrange
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css")); var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
@ -317,8 +374,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
// Act // Act
var urlList = globbingUrlBuilder.BuildUrlList( var urlList = globbingUrlBuilder.BuildUrlList(
staticUrl: null, staticUrl: null,
includePattern: $"{leadingSlash}**/*.css", includePattern: $"{prefix}**/*.css",
excludePattern: $"{leadingSlash}**/*.min.css"); excludePattern: $"{prefix}**/*.min.css");
// Assert // Assert
Assert.Collection(includePatterns, pattern => Assert.Equal("**/*.css", pattern)); Assert.Collection(includePatterns, pattern => Assert.Equal("**/*.css", pattern));
@ -326,12 +383,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
} }
[Theory] [Theory]
[InlineData("~/")]
[InlineData("/")] [InlineData("/")]
[InlineData("\\")] [InlineData("\\")]
public void TrimsOnlySingleLeadingSlashFromPatterns(string leadingSlash) public void TrimsOnlySingleLeadingSlashOrTildeSlashFromPatterns(string prefix)
{ {
// Arrange // Arrange
var leadingSlashes = $"{leadingSlash}{leadingSlash}"; var leadingSlashes = $"{prefix}{prefix}";
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css")); var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
IMemoryCache cache = null; IMemoryCache cache = null;
var requestPathBase = PathString.Empty; var requestPathBase = PathString.Empty;
@ -348,8 +406,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
excludePattern: $"{leadingSlashes}**/*.min.css"); excludePattern: $"{leadingSlashes}**/*.min.css");
// Assert // Assert
Assert.Collection(includePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.css", pattern)); Assert.Collection(includePatterns, pattern => Assert.Equal($"{prefix}**/*.css", pattern));
Assert.Collection(excludePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.min.css", pattern)); Assert.Collection(excludePatterns, pattern => Assert.Equal($"{prefix}**/*.min.css", pattern));
} }
public class FileNode public class FileNode

View File

@ -12,8 +12,11 @@
<!-- Globbed link tag with existing file --> <!-- Globbed link tag with existing file -->
<link asp-href-include="**/site.css" rel="stylesheet" title="&lt;the title>" /> <link asp-href-include="**/site.css" rel="stylesheet" title="&lt;the title>" />
<!-- Globbed link tag with comma separated pattern -->
<link asp-href-include="~/*.css, ~/sub/*.css" rel="stylesheet" title="&lt;the title" />
<!-- Globbed link tag with existing file and exclude --> <!-- Globbed link tag with existing file and exclude -->
<link asp-href-include="**/*.css" asp-href-exclude="**/site3*.css" rel="stylesheet" title='"the" title' /> <link asp-href-include=" **/*.css" asp-href-exclude="**/site3*.css" rel="stylesheet" title='"the" title' />
<!-- Globbed link tag missing include --> <!-- Globbed link tag missing include -->
<link asp-href-exclude="**/site2.css" rel="stylesheet" /> <link asp-href-exclude="**/site2.css" rel="stylesheet" />
@ -51,7 +54,7 @@
asp-fallback-test-value="hidden" /> asp-fallback-test-value="hidden" />
<!-- Fallback from globbed href with exclude to static href --> <!-- Fallback from globbed href with exclude to static href -->
<link asp-href-include="**/*.min.css" asp-href-exclude="**/site3.min.css" rel="stylesheet" data-extra="test" <link asp-href-include="**/*.min.css" asp-href-exclude=" **/site3.min.css" rel="stylesheet" data-extra="test"
asp-fallback-href="~/site.css" asp-fallback-href="~/site.css"
asp-fallback-test-class="hidden" asp-fallback-test-class="hidden"
asp-fallback-test-property="visibility" asp-fallback-test-property="visibility"

View File

@ -89,6 +89,9 @@
// Globbed script tag missing include but with static src // Globbed script tag missing include but with static src
</script> </script>
<!-- Globbed script tag with comma separated include pattern -->
<script asp-src-include="~/*.js, ~/sub/*.js"></script>
<!-- Globbed script tag with missing file --> <!-- Globbed script tag with missing file -->
<script asp-src-include="**/notThere.js"></script> <script asp-src-include="**/notThere.js"></script>