This repository has been archived by the owner on Dec 14, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
6d228a6
commit 0ef68ee
Showing
23 changed files
with
831 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
src/Microsoft.AspNet.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <summary> | ||
/// <see cref="ITagHelper"/> implementation targeting elements containing attributes with URL expected values. | ||
/// </summary> | ||
/// <remarks>Resolves URLs starting with '~/' (relative to the application's 'webroot' setting) that are not | ||
/// targeted by other <see cref="ITagHelper"/>s. Runs prior to other <see cref="ITagHelper"/>s to ensure | ||
/// application-relative URLs are resolved.</remarks> | ||
[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<string, IEnumerable<string>> ElementAttributeLookups = | ||
new Dictionary<string, IEnumerable<string>>(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" } }, | ||
}; | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="UrlResolutionTagHelper"/>. | ||
/// </summary> | ||
/// <param name="urlHelper">The <see cref="IUrlHelper"/>.</param> | ||
/// <param name="htmlEncoder">The <see cref="IHtmlEncoder"/>.</param> | ||
public UrlResolutionTagHelper(IUrlHelper urlHelper, IHtmlEncoder htmlEncoder) | ||
{ | ||
UrlHelper = urlHelper; | ||
HtmlEncoder = htmlEncoder; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override int Order | ||
{ | ||
get | ||
{ | ||
return DefaultOrder.DefaultFrameworkSortOrder - 999; | ||
} | ||
} | ||
|
||
protected IUrlHelper UrlHelper { get; } | ||
|
||
protected IHtmlEncoder HtmlEncoder { get; } | ||
|
||
/// <inheritdoc /> | ||
public override void Process(TagHelperContext context, TagHelperOutput output) | ||
{ | ||
IEnumerable<string> 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); | ||
} | ||
|
||
/// <summary> | ||
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for | ||
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose | ||
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>. | ||
/// </summary> | ||
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param> | ||
/// <param name="output">The <see cref="TagHelperOutput"/>.</param> | ||
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output) | ||
{ | ||
IEnumerable<TagHelperAttribute> 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); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting. | ||
/// </summary> | ||
/// <param name="url">The URL to resolve.</param> | ||
/// <param name="encodeWebRoot">If <c>true</c>, will HTML encode the expansion of '~/'.</param> | ||
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if | ||
/// <paramref name="url"/> could not be resolved.</param> | ||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns> | ||
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.