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
-
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.html
new file mode 100644
index 0000000000..0142ae93ea
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/RazorWebSite.UrlResolution.Index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Person
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs
new file mode 100644
index 0000000000..346db8856e
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs
@@ -0,0 +1,170 @@
+// 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 Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+using Microsoft.Framework.WebEncoders;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
+{
+ public class UrlResolutionTagHelperTest
+ {
+ public static TheoryData ResolvableUrlData
+ {
+ get
+ {
+ // url, expectedHref
+ return new TheoryData