From 0ef68eefc898247fe6a3c6d6a8ab8820ea2b34e4 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 16 Jul 2015 12:39:33 -0700 Subject: [PATCH] Added default `UrlResolutionTagHelper` to resolve app relative URLs. - Razor removed the ability to automatically resolve URLs prefixed with `~/`; therefore `ScriptTagHelper`, `LinkTagHelper` and `ImageTagHelper` have changed to take in `IUrlHelper`s and auto-resolve their URL based properties if they start with `~/`. - Added a catch-all `~/` resolver for non `TagHelper` based HTML elements. Razor used to resolve any attribute value that started with `~/` now the behavior is restricted to attributes that can contain URLs. - Updated `IUrlHelper` to accept `null` values. - Added functional tests to validate that URLs resolve correctly. - Updated `TagHelper` tests to ensure that URLs are resolved via an `IUrlHelper`. #2807 --- src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs | 7 +- .../MvcRazorHost.cs | 7 +- .../Properties/Resources.Designer.cs | 20 ++ src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 59 ++--- .../TagHelpers/UrlResolutionTagHelper.cs | 218 ++++++++++++++++++ .../ImageTagHelper.cs | 20 +- .../LinkTagHelper.cs | 46 +++- .../ScriptTagHelper.cs | 44 +++- .../UrlHelperTest.cs | 2 + .../UrlResolutionTest.cs | 71 ++++++ ...tion_Home.AttributesWithBooleanValues.html | 1 - ...tionWebSite.HtmlGeneration_Home.Image.html | 2 +- ...Site.HtmlGeneration_Home.Link.Encoded.html | 2 +- ...te.HtmlGeneration_Home.Script.Encoded.html | 2 +- ...orWebSite.UrlResolution.Index.Encoded.html | 32 +++ .../RazorWebSite.UrlResolution.Index.html | 32 +++ .../TagHelpers/UrlResolutionTagHelperTest.cs | 170 ++++++++++++++ .../ImageTagHelperTest.cs | 20 +- .../LinkTagHelperTest.cs | 44 +++- .../ScriptTagHelperTest.cs | 53 +++-- .../Views/HtmlGeneration_Home/Image.cshtml | 2 +- .../Controllers/UrlResolutionController.cs | 20 ++ .../Views/UrlResolution/Index.cshtml | 38 +++ 23 files changed, 831 insertions(+), 81 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/UrlResolutionTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.Encoded.html create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.html create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs create mode 100644 test/WebSites/RazorWebSite/Controllers/UrlResolutionController.cs create mode 100644 test/WebSites/RazorWebSite/Views/UrlResolution/Index.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs index 5f787503fc..186cceaf7a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs @@ -120,8 +120,13 @@ namespace Microsoft.AspNet.Mvc } /// - public virtual string Content([NotNull] string contentPath) + public virtual string Content(string contentPath) { + if (contentPath == null) + { + return null; + } + return GenerateClientUrl(_httpContext.Request.PathBase, contentPath); } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 2e7e797fd0..8a27fe01a1 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -31,12 +31,16 @@ namespace Microsoft.AspNet.Mvc.Razor "Microsoft.AspNet.Mvc", "Microsoft.AspNet.Mvc.Rendering", }; - private static readonly Chunk[] _defaultInheritedChunks = new[] + private static readonly Chunk[] _defaultInheritedChunks = new Chunk[] { new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper", HtmlHelperPropertyName), new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IJsonHelper", "Json"), new InjectChunk("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"), new InjectChunk("Microsoft.AspNet.Mvc.IUrlHelper", "Url"), + new AddTagHelperChunk + { + LookupText = "Microsoft.AspNet.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNet.Mvc.Razor" + }, }; // CodeGenerationContext.DefaultBaseClass is set to MyBaseType. @@ -99,7 +103,6 @@ namespace Microsoft.AspNet.Mvc.Razor MarkAsHtmlEncodedMethodName = HtmlHelperPropertyName + ".Raw", }) { - ResolveUrlMethodName = "Href", BeginContextMethodName = "BeginContext", EndContextMethodName = "EndContext" }; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index d2662b654c..a76d917405 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -426,6 +426,26 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_InvalidTagHelperIndexerAssignment"), p0, p1, p2); } + /// + /// Unexpected return value from '{1}.{2}' for URL '{0}'. If the '{1}' service has been overridden, change '{2}' to replace only the '~/' prefix. Otherwise, add the following directive to the Razor page to disable URL resolution relative to the application's 'webroot' setting: + /// + /// @{3} "{4}, {5}" + /// + internal static string CouldNotResolveApplicationRelativeUrl_TagHelper + { + get { return GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"); } + } + + /// + /// Unexpected return value from '{1}.{2}' for URL '{0}'. If the '{1}' service has been overridden, change '{2}' to replace only the '~/' prefix. Otherwise, add the following directive to the Razor page to disable URL resolution relative to the application's 'webroot' setting: + /// + /// @{3} "{4}, {5}" + /// + internal static string FormatCouldNotResolveApplicationRelativeUrl_TagHelper(object p0, object p1, object p2, object p3, object p4, object p5) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"), p0, p1, p2, p3, p4, p5); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 7da90b8653..bd3dede0e4 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -1,17 +1,17 @@  - @@ -195,4 +195,9 @@ Unable to perform '{0}' assignment. Tag helper property '{1}.{2}' must not be null. + + Unexpected return value from '{1}.{2}' for URL '{0}'. If the '{1}' service has been overridden, change '{2}' to replace only the '~/' prefix. Otherwise, add the following directive to the Razor page to disable URL resolution relative to the application's 'webroot' setting: + +@{3} "{4}, {5}" + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs b/src/Microsoft.AspNet.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs new file mode 100644 index 0000000000..b3dc9cd61b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs @@ -0,0 +1,218 @@ +// 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.ComponentModel; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Mvc.Razor.TagHelpers +{ + /// + /// implementation targeting elements containing attributes with URL expected values. + /// + /// Resolves URLs starting with '~/' (relative to the application's 'webroot' setting) that are not + /// targeted by other s. Runs prior to other s to ensure + /// application-relative URLs are resolved. + [TargetElement("*", Attributes = "itemid")] + [TargetElement("a", Attributes = "href")] + [TargetElement("applet", Attributes = "archive")] + [TargetElement("area", Attributes = "href")] + [TargetElement("audio", Attributes = "src")] + [TargetElement("base", Attributes = "href")] + [TargetElement("blockquote", Attributes = "cite")] + [TargetElement("button", Attributes = "formaction")] + [TargetElement("del", Attributes = "cite")] + [TargetElement("embed", Attributes = "src")] + [TargetElement("form", Attributes = "action")] + [TargetElement("html", Attributes = "manifest")] + [TargetElement("iframe", Attributes = "src")] + [TargetElement("img", Attributes = "src")] + [TargetElement("input", Attributes = "src")] + [TargetElement("input", Attributes = "formaction")] + [TargetElement("ins", Attributes = "cite")] + [TargetElement("link", Attributes = "href")] + [TargetElement("menuitem", Attributes = "icon")] + [TargetElement("object", Attributes = "archive")] + [TargetElement("object", Attributes = "data")] + [TargetElement("q", Attributes = "cite")] + [TargetElement("script", Attributes = "src")] + [TargetElement("source", Attributes = "src")] + [TargetElement("track", Attributes = "src")] + [TargetElement("video", Attributes = "src")] + [TargetElement("video", Attributes = "poster")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class UrlResolutionTagHelper : TagHelper + { + // Valid whitespace characters defined by the HTML5 spec. + private static readonly char[] ValidAttributeWhitespaceChars = + new[] { '\t', '\n', '\u000C', '\r', ' ' }; + private static readonly IReadOnlyDictionary> ElementAttributeLookups = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "a", new[] { "href" } }, + { "applet", new[] { "archive" } }, + { "area", new[] { "href" } }, + { "audio", new[] { "src" } }, + { "base", new[] { "href" } }, + { "blockquote", new[] { "cite" } }, + { "button", new[] { "formaction" } }, + { "del", new[] { "cite" } }, + { "embed", new[] { "src" } }, + { "form", new[] { "action" } }, + { "html", new[] { "manifest" } }, + { "iframe", new[] { "src" } }, + { "img", new[] { "src" } }, + { "input", new[] { "src", "formaction" } }, + { "ins", new[] { "cite" } }, + { "link", new[] { "href" } }, + { "menuitem", new[] { "icon" } }, + { "object", new[] { "archive", "data" } }, + { "q", new[] { "cite" } }, + { "script", new[] { "src" } }, + { "source", new[] { "src" } }, + { "track", new[] { "src" } }, + { "video", new[] { "poster", "src" } }, + }; + + /// + /// Creates a new . + /// + /// The . + /// The . + public UrlResolutionTagHelper(IUrlHelper urlHelper, IHtmlEncoder htmlEncoder) + { + UrlHelper = urlHelper; + HtmlEncoder = htmlEncoder; + } + + /// + public override int Order + { + get + { + return DefaultOrder.DefaultFrameworkSortOrder - 999; + } + } + + protected IUrlHelper UrlHelper { get; } + + protected IHtmlEncoder HtmlEncoder { get; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + IEnumerable attributeNames; + if (ElementAttributeLookups.TryGetValue(output.TagName, out attributeNames)) + { + foreach (var attributeName in attributeNames) + { + ProcessUrlAttribute(attributeName, output); + } + } + + // itemid can be present on any HTML element. + ProcessUrlAttribute("itemid", output); + } + + /// + /// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for + /// 's whose + /// is . + /// + /// The attribute name used to lookup values to resolve. + /// The . + protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output) + { + IEnumerable attributes; + if (output.Attributes.TryGetAttributes(attributeName, out attributes)) + { + foreach (var attribute in attributes) + { + string resolvedUrl; + + var stringValue = attribute.Value as string; + if (stringValue != null) + { + if (TryResolveUrl(stringValue, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) + { + attribute.Value = resolvedUrl; + } + } + else + { + var htmlStringValue = attribute.Value as HtmlString; + if (htmlStringValue != null && + TryResolveUrl( + htmlStringValue.ToString(), + encodeWebRoot: true, + resolvedUrl: out resolvedUrl)) + { + attribute.Value = new HtmlString(resolvedUrl); + } + } + } + } + } + + /// + /// Tries to resolve the given value relative to the application's 'webroot' setting. + /// + /// The URL to resolve. + /// If true, will HTML encode the expansion of '~/'. + /// Absolute URL beginning with the application's virtual root. null if + /// could not be resolved. + /// true if the could be resolved; false otherwise. + protected bool TryResolveUrl(string url, bool encodeWebRoot, out string resolvedUrl) + { + resolvedUrl = null; + + if (url == null) + { + return false; + } + + var trimmedUrl = url.Trim(ValidAttributeWhitespaceChars); + + // Before doing more work, ensure that the URL we're looking at is app relative. + if (trimmedUrl.Length >= 2 && trimmedUrl[0] == '~' && trimmedUrl[1] == '/') + { + var appRelativeUrl = UrlHelper.Content(trimmedUrl); + + if (encodeWebRoot) + { + var postTildeSlashUrlValue = trimmedUrl.Substring(2); + + if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + Resources.FormatCouldNotResolveApplicationRelativeUrl_TagHelper( + url, + nameof(IUrlHelper), + nameof(IUrlHelper.Content), + "removeTagHelper", + typeof(UrlResolutionTagHelper).FullName, + typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly.GetName().Name)); + } + + var applicationPath = appRelativeUrl.Substring(0, appRelativeUrl.Length - postTildeSlashUrlValue.Length); + var encodedApplicationPath = HtmlEncoder.HtmlEncode(applicationPath); + + resolvedUrl = string.Concat(encodedApplicationPath, postTildeSlashUrlValue); + } + else + { + resolvedUrl = appRelativeUrl; + } + + return true; + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs index f25239ac88..28680f0647 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs @@ -1,11 +1,12 @@ // 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 Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Mvc.Razor.TagHelpers; using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Caching.Memory; +using Microsoft.Framework.WebEncoders; namespace Microsoft.AspNet.Mvc.TagHelpers { @@ -16,7 +17,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// The tag helper won't process for cases with just the 'src' attribute. /// [TargetElement("img", Attributes = AppendVersionAttributeName + "," + SrcAttributeName)] - public class ImageTagHelper : TagHelper + public class ImageTagHelper : UrlResolutionTagHelper { private static readonly string Namespace = typeof(ImageTagHelper).Namespace; @@ -30,7 +31,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// /// The . /// The . - public ImageTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache) + /// The . + public ImageTagHelper( + IHostingEnvironment hostingEnvironment, + IMemoryCache cache, + IHtmlEncoder htmlEncoder, + IUrlHelper urlHelper) + : base(urlHelper, htmlEncoder) { HostingEnvironment = hostingEnvironment; Cache = cache; @@ -68,12 +75,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (AppendVersion) { EnsureFileVersionProvider(); + + string resolvedUrl; + if (TryResolveUrl(Src, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) + { + Src = resolvedUrl; + } output.Attributes[SrcAttributeName] = _fileVersionProvider.AddFileVersionToPath(Src); } else { // Pass through attribute that is also a well-known HTML attribute. output.CopyHtmlAttribute(SrcAttributeName, context); + ProcessUrlAttribute(SrcAttributeName, output); } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs index 28d21b585f..345905123c 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -2,11 +2,11 @@ // 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.Diagnostics; using System.Globalization; using System.Linq; using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Mvc.Razor.TagHelpers; using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Caching.Memory; @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [TargetElement("link", Attributes = FallbackTestPropertyAttributeName)] [TargetElement("link", Attributes = FallbackTestValueAttributeName)] [TargetElement("link", Attributes = AppendVersionAttributeName)] - public class LinkTagHelper : TagHelper + public class LinkTagHelper : UrlResolutionTagHelper { private static readonly string Namespace = typeof(LinkTagHelper).Namespace; @@ -91,17 +91,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// The . /// The . /// The . + /// The . public LinkTagHelper( ILogger logger, IHostingEnvironment hostingEnvironment, IMemoryCache cache, IHtmlEncoder htmlEncoder, - IJavaScriptStringEncoder javaScriptEncoder) + IJavaScriptStringEncoder javaScriptEncoder, + IUrlHelper urlHelper) + : base(urlHelper, htmlEncoder) { Logger = logger; HostingEnvironment = hostingEnvironment; Cache = cache; - HtmlEncoder = htmlEncoder; JavaScriptEncoder = javaScriptEncoder; } @@ -195,8 +197,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers protected IMemoryCache Cache { get; } - protected IHtmlEncoder HtmlEncoder { get; } - protected IJavaScriptStringEncoder JavaScriptEncoder { get; } // Internal for ease of use when testing. @@ -205,10 +205,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// public override void Process(TagHelperContext context, TagHelperOutput output) { + string resolvedUrl; + // Pass through attribute that is also a well-known HTML attribute. if (Href != null) { output.CopyHtmlAttribute(HrefAttributeName, context); + + // Resolve any application relative URLs (~/) now so they can be used in comparisons later. + if (TryResolveUrl(Href, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) + { + Href = resolvedUrl; + } + + ProcessUrlAttribute(HrefAttributeName, output); } var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); @@ -243,6 +253,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers 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); if (string.IsNullOrEmpty(Href)) { @@ -254,6 +273,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (mode == Mode.Fallback) { + if (TryResolveUrl(FallbackHref, encodeWebRoot: false, resolvedUrl: out 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); } @@ -292,7 +324,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { if (AppendVersion == true) { - for (var i=0; i < fallbackHrefs.Length; i++) + for (var i = 0; i < fallbackHrefs.Length; i++) { // fallbackHrefs come from bound attributes and globbing. Must always be non-null. Debug.Assert(fallbackHrefs[i] != null); diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs index 242aaa90fa..19729ad61f 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs @@ -2,11 +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.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Mvc.Razor.TagHelpers; using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Caching.Memory; @@ -28,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [TargetElement("script", Attributes = FallbackSrcExcludeAttributeName)] [TargetElement("script", Attributes = FallbackTestExpressionAttributeName)] [TargetElement("script", Attributes = AppendVersionAttributeName)] - public class ScriptTagHelper : TagHelper + public class ScriptTagHelper : UrlResolutionTagHelper { private const string SrcIncludeAttributeName = "asp-src-include"; private const string SrcExcludeAttributeName = "asp-src-exclude"; @@ -78,17 +77,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// The . /// The . /// The . + /// The . public ScriptTagHelper( ILogger logger, IHostingEnvironment hostingEnvironment, IMemoryCache cache, IHtmlEncoder htmlEncoder, - IJavaScriptStringEncoder javaScriptEncoder) + IJavaScriptStringEncoder javaScriptEncoder, + IUrlHelper urlHelper) + : base(urlHelper, htmlEncoder) { Logger = logger; HostingEnvironment = hostingEnvironment; Cache = cache; - HtmlEncoder = htmlEncoder; JavaScriptEncoder = javaScriptEncoder; } @@ -164,8 +165,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers protected IMemoryCache Cache { get; } - protected IHtmlEncoder HtmlEncoder { get; } - protected IJavaScriptStringEncoder JavaScriptEncoder { get; } // Internal for ease of use when testing. @@ -174,10 +173,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// public override void Process(TagHelperContext context, TagHelperOutput output) { + string resolvedUrl; + // Pass through attribute that is also a well-known HTML attribute. if (Src != null) { output.CopyHtmlAttribute(SrcAttributeName, context); + + if (TryResolveUrl(Src, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) + { + Src = resolvedUrl; + } + + ProcessUrlAttribute(SrcAttributeName, output); } var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); @@ -212,6 +220,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers 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); if (string.IsNullOrEmpty(Src)) { @@ -223,6 +240,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (mode == Mode.Fallback) { + if (TryResolveUrl(FallbackSrc, encodeWebRoot: false, resolvedUrl: out 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); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs index fe4c55c173..0abc6e6216 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs @@ -19,6 +19,8 @@ namespace Microsoft.AspNet.Mvc public class UrlHelperTest { [Theory] + [InlineData(null, null, null)] + [InlineData("/myapproot", null, null)] [InlineData("", "/Home/About", "/Home/About")] [InlineData("/myapproot", "/test", "/test")] public void Content_ReturnsContentPath_WhenItDoesNotStartWithToken(string appRoot, diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/UrlResolutionTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/UrlResolutionTest.cs new file mode 100644 index 0000000000..e222c49f85 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/UrlResolutionTest.cs @@ -0,0 +1,71 @@ +// 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.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.WebEncoders; +using RazorWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class UrlResolutionTest + { + private const string SiteName = nameof(RazorWebSite); + private readonly Action _app = new Startup().Configure; + private readonly Action _configureServices = new Startup().ConfigureServices; + private static readonly Assembly _resourcesAssembly = typeof(UrlResolutionTest).GetTypeInfo().Assembly; + + [Fact] + public async Task AppRelativeUrlsAreResolvedCorrectly() + { + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + + var client = server.CreateClient(); + var outputFile = "compiler/resources/RazorWebSite.UrlResolution.Index.html"; + var expectedContent = + await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false); + + // Act + var response = await client.GetAsync("http://localhost/UrlResolution/Index"); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + responseContent = responseContent.Trim(); +#if GENERATE_BASELINES + ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent); +#else + Assert.Equal(expectedContent.Trim(), responseContent, ignoreLineEndingDifferences: true); +#endif + } + + [Fact] + public async Task AppRelativeUrlsAreResolvedAndEncodedCorrectly() + { + var server = TestHelper.CreateServer(_app, SiteName, services => + { + _configureServices(services); + services.AddTransient(); + }); + var client = server.CreateClient(); + var outputFile = "compiler/resources/RazorWebSite.UrlResolution.Index.Encoded.html"; + var expectedContent = + await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false); + + // Act + var response = await client.GetAsync("http://localhost/UrlResolution/Index"); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + responseContent = responseContent.Trim(); +#if GENERATE_BASELINES + ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent); +#else + Assert.Equal(expectedContent.Trim(), responseContent, ignoreLineEndingDifferences: true); +#endif + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.AttributesWithBooleanValues.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.AttributesWithBooleanValues.html index 1882d6d649..4cdd10a012 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.AttributesWithBooleanValues.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.AttributesWithBooleanValues.html @@ -1,4 +1,3 @@ - diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Image.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Image.html index 1a429f0103..aa872be294 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Image.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Image.html @@ -9,7 +9,7 @@

Image Tag Helper Test

- Red block + Red block Red versioned diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html index b3cca4acda..49e37c2fe9 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html @@ -5,7 +5,7 @@ Link - + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html index 0aba62fd84..071a745a64 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html @@ -6,7 +6,7 @@

Script tag helper test

- diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.Encoded.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.Encoded.html new file mode 100644 index 0000000000..48c341cc8a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.Encoded.html @@ -0,0 +1,32 @@ + + + + + +
+ Person + Url stuff + +