Clean up remaining `HtmlString` usage

- #3122 and #3123 (5 of 5)
- avoid concatenating values and wrapping them in an `HtmlString`
  - switch from `string.Format()` to `AppendHtml()` in `LinkTagHelper`
  - simplify `RazorPage` and `TagHelperContentExtensions` e.g. don't use a `TagHelperContentWrapperTextWriter`
  - add some special cases in `UrlResolutionTagHelper`
- add `TagBuilder` and `StringHtmlContent` special cases to avoid `StringWriter` use when value is an `HtmlString`
- move `HtmlTextWriter` optimizations a bit lower and add missing checks for that type

nits:
- correct comments that incorrectly mention `HtmlString`s
- update comments in `JavaScriptStringArrayEncoder`
This commit is contained in:
Doug Bunting 2015-12-03 15:35:42 -08:00
parent dd774366f6
commit 1f76b3c7b7
17 changed files with 333 additions and 174 deletions

View File

@ -602,22 +602,10 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
if (!_tagHelperAttributeInfo.Suppressed) if (!_tagHelperAttributeInfo.Suppressed)
{ {
HtmlString htmlString; executionContext.AddHtmlAttribute(
_tagHelperAttributeInfo.Name,
if (_valueBuffer != null) _valueBuffer?.Content ?? HtmlString.Empty);
{ _valueBuffer = null;
using (var stringWriter = new StringWriter())
{
_valueBuffer.Content.WriteTo(stringWriter, HtmlEncoder);
htmlString = new HtmlString(stringWriter.ToString());
}
}
else
{
htmlString = HtmlString.Empty;
}
executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, htmlString);
} }
} }
@ -843,7 +831,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// content to the <see cref="HttpResponse.Body"/>. /// content to the <see cref="HttpResponse.Body"/>.
/// </summary> /// </summary>
/// <returns>A <see cref="Task{HtmlString}"/> that represents the asynchronous flush operation and on /// <returns>A <see cref="Task{HtmlString}"/> that represents the asynchronous flush operation and on
/// completion returns a <see cref="HtmlString.Empty"/>.</returns> /// completion returns <see cref="HtmlString.Empty"/>.</returns>
/// <remarks>The value returned is a token value that allows FlushAsync to work directly in an HTML /// <remarks>The value returned is a token value that allows FlushAsync to work directly in an HTML
/// section. However the value does not represent the rendered content. /// section. However the value does not represent the rendered content.
/// This method also writes out headers, so any modifications to headers must be done before /// This method also writes out headers, so any modifications to headers must be done before
@ -936,7 +924,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Sets antiforgery cookie and X-Frame-Options header on the response. /// Sets antiforgery cookie and X-Frame-Options header on the response.
/// </summary> /// </summary>
/// <returns>A <see cref="HtmlString"/> that returns a <see cref="HtmlString.Empty"/>.</returns> /// <returns><see cref="HtmlString.Empty"/>.</returns>
/// <remarks> Call this method to send antiforgery cookie token and X-Frame-Options header to client /// <remarks> Call this method to send antiforgery cookie token and X-Frame-Options header to client
/// before <see cref="FlushAsync"/> flushes the headers. </remarks> /// before <see cref="FlushAsync"/> flushes the headers. </remarks>
public virtual HtmlString SetAntiforgeryCookieAndHeader() public virtual HtmlString SetAntiforgeryCookieAndHeader()

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Text; using System.Text;
using Microsoft.AspNet.Html; using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor

View File

@ -4,8 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
@ -160,26 +162,45 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
{ {
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
string resolvedUrl;
var stringValue = attribute.Value as string; var stringValue = attribute.Value as string;
if (stringValue != null) if (stringValue != null)
{ {
if (TryResolveUrl(stringValue, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) string resolvedUrl;
if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl))
{ {
attribute.Value = resolvedUrl; attribute.Value = resolvedUrl;
} }
} }
else else
{ {
var htmlStringValue = attribute.Value as HtmlString; var htmlContent = attribute.Value as IHtmlContent;
if (htmlStringValue != null && if (htmlContent != null)
TryResolveUrl(
htmlStringValue.ToString(),
encodeWebRoot: true,
resolvedUrl: out resolvedUrl))
{ {
attribute.Value = new HtmlString(resolvedUrl); var htmlString = htmlContent as HtmlString;
if (htmlString != null)
{
// No need for a StringWriter in this case.
stringValue = htmlString.ToString();
}
else
{
using (var writer = new StringWriter())
{
htmlContent.WriteTo(writer, HtmlEncoder);
stringValue = writer.ToString();
}
}
IHtmlContent resolvedUrl;
if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl))
{
attribute.Value = resolvedUrl;
}
else if (htmlString == null)
{
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
attribute.Value = new HtmlString(stringValue);
}
} }
} }
} }
@ -190,14 +211,12 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting. /// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
/// </summary> /// </summary>
/// <param name="url">The URL to resolve.</param> /// <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 /// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
/// <paramref name="url"/> could not be resolved.</param> /// <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> /// <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) protected bool TryResolveUrl(string url, out string resolvedUrl)
{ {
resolvedUrl = null; resolvedUrl = null;
if (url == null) if (url == null)
{ {
return false; return false;
@ -205,42 +224,93 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
var trimmedUrl = url.Trim(ValidAttributeWhitespaceChars); var trimmedUrl = url.Trim(ValidAttributeWhitespaceChars);
// Before doing more work, ensure that the URL we're looking at is app relative. // Before doing more work, ensure that the URL we're looking at is app-relative.
if (trimmedUrl.Length >= 2 && trimmedUrl[0] == '~' && trimmedUrl[1] == '/') if (trimmedUrl.Length >= 2 && trimmedUrl[0] == '~' && trimmedUrl[1] == '/')
{ {
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var appRelativeUrl = urlHelper.Content(trimmedUrl); resolvedUrl = 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.Encode(applicationPath);
resolvedUrl = string.Concat(encodedApplicationPath, postTildeSlashUrlValue);
}
else
{
resolvedUrl = appRelativeUrl;
}
return true; return true;
} }
return false; return false;
} }
/// <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="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, out IHtmlContent 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 urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var appRelativeUrl = urlHelper.Content(trimmedUrl);
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));
}
resolvedUrl = new EncodeFirstSegmentContent(
appRelativeUrl,
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
postTildeSlashUrlValue);
return true;
}
return false;
}
private class EncodeFirstSegmentContent : IHtmlContent
{
private readonly string _firstSegment;
private readonly int _firstSegmentLength;
private readonly string _secondSegment;
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
{
_firstSegment = firstSegment;
_firstSegmentLength = firstSegmentLength;
_secondSegment = secondSegment;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
var htmlTextWriter = writer as HtmlTextWriter;
if (htmlTextWriter != null)
{
htmlTextWriter.Write(this);
}
else
{
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
writer.Write(_secondSegment);
}
}
}
} }
} }

View File

@ -38,7 +38,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
// Load the JavaScript from embedded resource // Load the JavaScript from embedded resource
using (var resourceStream = getManifestResourceStream(key)) using (var resourceStream = getManifestResourceStream(key))
{ {
Debug.Assert(resourceStream != null, "Embedded resource missing. Ensure 'prebuild' script has run."); Debug.Assert(
resourceStream != null,
"Embedded resource missing. Ensure 'prebuild' script has run.");
using (var streamReader = new StreamReader(resourceStream)) using (var streamReader = new StreamReader(resourceStream))
{ {
@ -49,14 +51,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
} }
}); });
} }
private static string PrepareFormatString(string input) private static string PrepareFormatString(string input)
{ {
// Replace unescaped/escaped chars with their equivalent // Remove final ");". Those characters are in the file only to allow minification.
return input.Replace("{", "{{") return input.Substring(0, input.Length - 2);
.Replace("}", "}}")
.Replace("[[[", "{")
.Replace("]]]", "}");
} }
} }
} }

View File

@ -1,35 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
/// <summary> /// <summary>
/// Methods for encoding <see cref="IEnumerable{string}"/> for use as a JavaScript array literal. /// Methods for encoding <c>string[]</c>s for use as a JavaScript array literal.
/// </summary> /// </summary>
public static class JavaScriptStringArrayEncoder public static class JavaScriptStringArrayEncoder
{ {
/// <summary> /// <summary>
/// Encodes a .NET string array for safe use as a JavaScript array literal, including inline in an HTML file. /// Encodes a <c>string[]</c> for safe use as a JavaScript array literal in many contexts, including
/// inline in an HTML file.
/// </summary> /// </summary>
public static string Encode(JavaScriptEncoder encoder, string[] values) public static string Encode(JavaScriptEncoder encoder, string[] values)
{ {
var writer = new StringWriter(); var writer = new StringWriter();
var firstAdded = false;
writer.Write('['); writer.Write('[');
// Perf: Avoid allocating enumerator // Perf: Avoid allocating enumerator
var firstAdded = false;
for (var i = 0; i < values.Length; i++) for (var i = 0; i < values.Length; i++)
{ {
if (firstAdded) if (firstAdded)
{ {
writer.Write(','); writer.Write(',');
} }
writer.Write('"'); writer.Write('"');
encoder.Encode(writer, values[i]); encoder.Encode(writer, values[i]);
writer.Write('"'); writer.Write('"');

View File

@ -12,7 +12,6 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Mvc.TagHelpers.Logging; using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -287,7 +286,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (mode == Mode.Fallback) if (mode == Mode.Fallback)
{ {
string resolvedUrl; string resolvedUrl;
if (TryResolveUrl(FallbackHref, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) if (TryResolveUrl(FallbackHref, resolvedUrl: out resolvedUrl))
{ {
FallbackHref = resolvedUrl; FallbackHref = resolvedUrl;
} }
@ -350,16 +349,17 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Build the <script /> tag that checks the effective style of <meta /> tag above and renders the extra // Build the <script /> tag that checks the effective style of <meta /> tag above and renders the extra
// <link /> tag to load the fallback stylesheet if the test CSS property value is found to be false, // <link /> tag to load the fallback stylesheet if the test CSS property value is found to be false,
// indicating that the primary stylesheet failed to load. // indicating that the primary stylesheet failed to load.
// GetEmbeddedJavaScript returns JavaScript to which we add '"{0}","{1}",{2});'
builder builder
.AppendHtml("<script>") .AppendHtml("<script>")
.AppendHtml( .AppendHtml(JavaScriptResources.GetEmbeddedJavaScript(FallbackJavaScriptResourceName))
string.Format( .AppendHtml("\"")
CultureInfo.InvariantCulture, .AppendHtml(JavaScriptEncoder.Encode(FallbackTestProperty))
JavaScriptResources.GetEmbeddedJavaScript(FallbackJavaScriptResourceName), .AppendHtml("\",\"")
JavaScriptEncoder.Encode(FallbackTestProperty), .AppendHtml(JavaScriptEncoder.Encode(FallbackTestValue))
JavaScriptEncoder.Encode(FallbackTestValue), .AppendHtml("\",")
JavaScriptStringArrayEncoder.Encode(JavaScriptEncoder, fallbackHrefs))) .AppendHtml(JavaScriptStringArrayEncoder.Encode(JavaScriptEncoder, fallbackHrefs))
.AppendHtml("</script>"); .AppendHtml(");</script>");
} }
} }
@ -410,7 +410,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
builder builder
.AppendHtml(attribute.Name) .AppendHtml(attribute.Name)
.AppendHtml("=\"") .AppendHtml("=\"")
.Append(HtmlEncoder, ViewContext.Writer.Encoding, attributeValue) .Append(HtmlEncoder, attributeValue)
.AppendHtml("\" "); .AppendHtml("\" ");
} }

View File

@ -254,7 +254,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (mode == Mode.Fallback) if (mode == Mode.Fallback)
{ {
string resolvedUrl; string resolvedUrl;
if (TryResolveUrl(FallbackSrc, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) if (TryResolveUrl(FallbackSrc, resolvedUrl: out resolvedUrl))
{ {
FallbackSrc = resolvedUrl; FallbackSrc = resolvedUrl;
} }
@ -418,10 +418,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
else else
{ {
// HTML-encoded the given value if necessary. // HTML-encode the given value if necessary.
content content
.AppendHtml("=\"") .AppendHtml("=\"")
.Append(HtmlEncoder, ViewContext.Writer.Encoding, value) .Append(HtmlEncoder, value)
.AppendHtml("\""); .AppendHtml("\"");
} }
} }

View File

@ -3,9 +3,9 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
@ -20,7 +20,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// </summary> /// </summary>
/// <param name="content">The <see cref="TagHelperContent"/> to write to.</param> /// <param name="content">The <see cref="TagHelperContent"/> to write to.</param>
/// <param name="encoder">The <see cref="HtmlEncoder"/> to use when encoding <paramref name="value"/>.</param> /// <param name="encoder">The <see cref="HtmlEncoder"/> to use when encoding <paramref name="value"/>.</param>
/// <param name="encoding">The character encoding in which the <paramref name="value"/> is written.</param>
/// <param name="value">The <see cref="object"/> to write.</param> /// <param name="value">The <see cref="object"/> to write.</param>
/// <returns><paramref name="content"/> after the write operation has completed.</returns> /// <returns><paramref name="content"/> after the write operation has completed.</returns>
/// <remarks> /// <remarks>
@ -29,11 +28,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// For all other types, the encoded result of <see cref="object.ToString"/> /// For all other types, the encoded result of <see cref="object.ToString"/>
/// is written to the <paramref name="content"/>. /// is written to the <paramref name="content"/>.
/// </remarks> /// </remarks>
public static TagHelperContent Append( public static TagHelperContent Append(this TagHelperContent content, HtmlEncoder encoder, object value)
this TagHelperContent content,
HtmlEncoder encoder,
Encoding encoding,
object value)
{ {
if (content == null) if (content == null)
{ {
@ -45,25 +40,34 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
if (encoding == null) if (value == null)
{ {
throw new ArgumentNullException(nameof(encoding)); // No real action but touch content to ensure IsModified is true.
content.Append((string)null);
return content;
} }
using (var writer = new TagHelperContentWrapperTextWriter(encoding, content)) string stringValue;
var htmlString = value as HtmlString;
if (htmlString != null)
{
// No need for a StringWriter in this case.
stringValue = htmlString.ToString();
}
else
{ {
using (var stringWriter = new StringWriter()) using (var stringWriter = new StringWriter())
{ {
RazorPage.WriteTo(stringWriter, encoder, value); RazorPage.WriteTo(stringWriter, encoder, value);
stringValue = stringWriter.ToString();
// In this case the text likely came directly from the Razor source. Since the original string is
// an attribute value that may have been quoted with single quotes, must handle any double quotes
// in the value. Writing the value out surrounded by double quotes.
var stringValue = stringWriter.ToString().Replace("\"", "&quot;");
writer.Write(stringValue);
} }
} }
// In this case the text likely came directly from the Razor source. Since the original string is
// an attribute value that may have been quoted with single quotes, must handle any double quotes
// in the value. Writing the value out surrounded by double quotes.
content.AppendHtml(stringValue.Replace("\"", "&quot;"));
return content; return content;
} }
} }

View File

@ -26,4 +26,4 @@
doc.write('<link rel="stylesheet" href="' + fallbackHref[i] + '"/>'); doc.write('<link rel="stylesheet" href="' + fallbackHref[i] + '"/>');
} }
} }
})("[[[0]]]", "[[[1]]]", [[[2]]]); })();

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>. /// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param> /// </param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that on completion returns a new <see cref="HtmlString"/> containing /// A <see cref="Task"/> that on completion returns a new <see cref="IHtmlContent"/> instance containing
/// the created HTML. /// the created HTML.
/// </returns> /// </returns>
public static Task<IHtmlContent> PartialAsync( public static Task<IHtmlContent> PartialAsync(
@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// </param> /// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param> /// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that on completion returns a new <see cref="HtmlString"/> containing /// A <see cref="Task"/> that on completion returns a new <see cref="IHtmlContent"/> instance containing
/// the created HTML. /// the created HTML.
/// </returns> /// </returns>
public static Task<IHtmlContent> PartialAsync( public static Task<IHtmlContent> PartialAsync(
@ -80,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// </param> /// </param>
/// <param name="model">A model to pass into the partial view.</param> /// <param name="model">A model to pass into the partial view.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that on completion returns a new <see cref="HtmlString"/> containing /// A <see cref="Task"/> that on completion returns a new <see cref="IHtmlContent"/> instance containing
/// the created HTML. /// the created HTML.
/// </returns> /// </returns>
public static Task<IHtmlContent> PartialAsync( public static Task<IHtmlContent> PartialAsync(
@ -109,7 +109,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>. /// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param> /// </param>
/// <returns> /// <returns>
/// Returns a new <see cref="HtmlString"/> containing the created HTML. /// Returns a new <see cref="IHtmlContent"/> instance containing the created HTML.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// This method synchronously calls and blocks on /// This method synchronously calls and blocks on
@ -141,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// </param> /// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param> /// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns> /// <returns>
/// Returns a new <see cref="HtmlString"/> containing the created HTML. /// Returns a new <see cref="IHtmlContent"/> instance containing the created HTML.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// This method synchronously calls and blocks on /// This method synchronously calls and blocks on
@ -174,7 +174,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// </param> /// </param>
/// <param name="model">A model to pass into the partial view.</param> /// <param name="model">A model to pass into the partial view.</param>
/// <returns> /// <returns>
/// Returns a new <see cref="HtmlString"/> containing the created HTML. /// Returns a new <see cref="IHtmlContent"/> instance containing the created HTML.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// This method synchronously calls and blocks on /// This method synchronously calls and blocks on
@ -208,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <param name="model">A model to pass into the partial view.</param> /// <param name="model">A model to pass into the partial view.</param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param> /// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns> /// <returns>
/// Returns a new <see cref="HtmlString"/> containing the created HTML. /// Returns a new <see cref="IHtmlContent"/> instance containing the created HTML.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// This method synchronously calls and blocks on /// This method synchronously calls and blocks on

View File

@ -498,7 +498,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <param name="model">A model to pass into the partial view.</param> /// <param name="model">A model to pass into the partial view.</param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param> /// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that on completion returns a new <see cref="HtmlString"/> containing /// A <see cref="Task"/> that on completion returns a new <see cref="IHtmlContent"/> instance containing
/// the created HTML. /// the created HTML.
/// </returns> /// </returns>
Task<IHtmlContent> PartialAsync(string partialViewName, object model, ViewDataDictionary viewData); Task<IHtmlContent> PartialAsync(string partialViewName, object model, ViewDataDictionary viewData);

View File

@ -10,7 +10,6 @@ using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Html; using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.Rendering namespace Microsoft.AspNet.Mvc.Rendering
{ {
@ -252,6 +251,15 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <inheritdoc /> /// <inheritdoc />
public void WriteTo(TextWriter writer, HtmlEncoder encoder) public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{ {
var htmlTextWriter = writer as HtmlTextWriter;
if (htmlTextWriter != null)
{
// As a perf optimization, we can buffer this output rather than writing it
// out character by character.
htmlTextWriter.Write(this);
return;
}
switch (TagRenderMode) switch (TagRenderMode)
{ {
case TagRenderMode.StartTag: case TagRenderMode.StartTag:

View File

@ -866,18 +866,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
if (tagBuilder != null) if (tagBuilder != null)
{ {
tagBuilder.TagRenderMode = TagRenderMode.StartTag; tagBuilder.TagRenderMode = TagRenderMode.StartTag;
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
// As a perf optimization, we can buffer this output rather than writing it
// out character by character.
var htmlWriter = ViewContext.Writer as HtmlTextWriter;
if (htmlWriter == null)
{
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
}
else
{
htmlWriter.Write(tagBuilder);
}
} }
return CreateForm(); return CreateForm();
@ -920,18 +909,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
if (tagBuilder != null) if (tagBuilder != null)
{ {
tagBuilder.TagRenderMode = TagRenderMode.StartTag; tagBuilder.TagRenderMode = TagRenderMode.StartTag;
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
// As a perf optimization, we can buffer this output rather than writing it
// out character by character.
var htmlWriter = ViewContext.Writer as HtmlTextWriter;
if (htmlWriter == null)
{
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
}
else
{
htmlWriter.Write(tagBuilder);
}
} }
return CreateForm(); return CreateForm();

View File

@ -39,7 +39,17 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
encoder.Encode(writer, _input); var htmlTextWriter = writer as HtmlTextWriter;
if (htmlTextWriter != null)
{
// As a perf optimization, we can buffer this output rather than writing it
// out character by character.
htmlTextWriter.Write(this);
}
else
{
encoder.Encode(writer, _input);
}
} }
private string DebuggerToString() private string DebuggerToString()

View File

@ -961,13 +961,14 @@ namespace Microsoft.AspNet.Mvc.Razor
// Assert // Assert
var htmlAttribute = Assert.Single(executionContext.HTMLAttributes); var htmlAttribute = Assert.Single(executionContext.HTMLAttributes);
Assert.Equal("someattr", htmlAttribute.Name, StringComparer.Ordinal); Assert.Equal("someattr", htmlAttribute.Name, StringComparer.Ordinal);
Assert.IsType<HtmlString>(htmlAttribute.Value); var htmlContent = Assert.IsAssignableFrom<IHtmlContent>(htmlAttribute.Value);
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString((IHtmlContent)htmlAttribute.Value)); Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(htmlContent), StringComparer.Ordinal);
Assert.False(htmlAttribute.Minimized); Assert.False(htmlAttribute.Minimized);
var allAttribute = Assert.Single(executionContext.AllAttributes); var allAttribute = Assert.Single(executionContext.AllAttributes);
Assert.Equal("someattr", allAttribute.Name, StringComparer.Ordinal); Assert.Equal("someattr", allAttribute.Name, StringComparer.Ordinal);
Assert.IsType<HtmlString>(allAttribute.Value); htmlContent = Assert.IsAssignableFrom<IHtmlContent>(allAttribute.Value);
Assert.Equal(expectedValue, allAttribute.Value.ToString(), StringComparer.Ordinal); Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(htmlContent), StringComparer.Ordinal);
Assert.False(allAttribute.Minimized); Assert.False(allAttribute.Minimized);
} }

View File

@ -6,8 +6,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.TestCommon;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
@ -22,22 +24,13 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
get get
{ {
// url, expectedHref // url, expectedHref
return new TheoryData<object, object> return new TheoryData<string, string>
{ {
{ "~/home/index.html", "/approot/home/index.html" }, { "~/home/index.html", "/approot/home/index.html" },
{ " ~/home/index.html", "/approot/home/index.html" }, { " ~/home/index.html", "/approot/home/index.html" },
{ new HtmlString("~/home/index.html"), new HtmlString("HtmlEncode[[/approot/]]home/index.html") },
{ {
new HtmlString(" ~/home/index.html"), "~/home/index.html ~/secondValue/index.html",
new HtmlString("HtmlEncode[[/approot/]]home/index.html") "/approot/home/index.html ~/secondValue/index.html"
},
{
"~/home/index.html ~/secondValue/index.html",
"/approot/home/index.html ~/secondValue/index.html"
},
{
new HtmlString("~/home/index.html ~/secondValue/index.html"),
new HtmlString("HtmlEncode[[/approot/]]home/index.html ~/secondValue/index.html")
}, },
}; };
} }
@ -45,7 +38,7 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
[Theory] [Theory]
[MemberData(nameof(ResolvableUrlData))] [MemberData(nameof(ResolvableUrlData))]
public void Process_ResolvesTildeSlashValues(object url, object expectedHref) public void Process_ResolvesTildeSlashValues(string url, string expectedHref)
{ {
// Arrange // Arrange
var tagHelperOutput = new TagHelperOutput( var tagHelperOutput = new TagHelperOutput(
@ -77,8 +70,64 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
// Assert // Assert
var attribute = Assert.Single(tagHelperOutput.Attributes); var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal("href", attribute.Name, StringComparer.Ordinal); Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
Assert.IsType(expectedHref.GetType(), url); var attributeValue = Assert.IsType<string>(attribute.Value);
Assert.Equal(expectedHref.ToString(), attribute.Value.ToString()); Assert.Equal(expectedHref, attributeValue, StringComparer.Ordinal);
Assert.False(attribute.Minimized);
}
public static TheoryData ResolvableUrlHtmlStringData
{
get
{
// url, expectedHref
return new TheoryData<HtmlString, string>
{
{ new HtmlString("~/home/index.html"), "HtmlEncode[[/approot/]]home/index.html" },
{ new HtmlString(" ~/home/index.html"), "HtmlEncode[[/approot/]]home/index.html" },
{
new HtmlString("~/home/index.html ~/secondValue/index.html"),
"HtmlEncode[[/approot/]]home/index.html ~/secondValue/index.html"
},
};
}
}
[Theory]
[MemberData(nameof(ResolvableUrlHtmlStringData))]
public void Process_ResolvesTildeSlashValues_InHtmlString(object url, string expectedHref)
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
tagName: "a",
attributes: new TagHelperAttributeList
{
{ "href", url }
},
getChildContentAsync: _ => Task.FromResult<TagHelperContent>(null));
var urlHelperMock = new Mock<IUrlHelper>();
urlHelperMock
.Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(value => "/approot" + value.Substring(1)));
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelperMock.Object);
var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
var context = new TagHelperContext(
allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
Enumerable.Empty<IReadOnlyTagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act
tagHelper.Process(context, tagHelperOutput);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
var htmlContent = Assert.IsAssignableFrom<IHtmlContent>(attribute.Value);
Assert.Equal(expectedHref, HtmlContentUtilities.HtmlContentToString(htmlContent), StringComparer.Ordinal);
Assert.False(attribute.Minimized); Assert.False(attribute.Minimized);
} }
@ -87,25 +136,20 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
get get
{ {
// url // url
return new TheoryData<object> return new TheoryData<string>
{ {
{ "/home/index.html" }, { "/home/index.html" },
{ "~ /home/index.html" }, { "~ /home/index.html" },
{ "/home/index.html ~/second/wontresolve.html" }, { "/home/index.html ~/second/wontresolve.html" },
{ " ~\\home\\index.html" }, { " ~\\home\\index.html" },
{ "~\\/home/index.html" }, { "~\\/home/index.html" },
{ new HtmlString("/home/index.html") },
{ new HtmlString("~ /home/index.html") },
{ new HtmlString("/home/index.html ~/second/wontresolve.html") },
{ new HtmlString("~\\home\\index.html") },
{ new HtmlString("~\\/home/index.html") },
}; };
} }
} }
[Theory] [Theory]
[MemberData(nameof(UnresolvableUrlData))] [MemberData(nameof(UnresolvableUrlData))]
public void Process_DoesNotResolveNonTildeSlashValues(object url) public void Process_DoesNotResolveNonTildeSlashValues(string url)
{ {
// Arrange // Arrange
var tagHelperOutput = new TagHelperOutput( var tagHelperOutput = new TagHelperOutput(
@ -123,7 +167,7 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
urlHelperFactory urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())) .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelperMock.Object); .Returns(urlHelperMock.Object);
var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, htmlEncoder: null); var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
var context = new TagHelperContext( var context = new TagHelperContext(
allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>( allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
@ -137,7 +181,63 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
// Assert // Assert
var attribute = Assert.Single(tagHelperOutput.Attributes); var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal("href", attribute.Name, StringComparer.Ordinal); Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
Assert.Equal(url, attribute.Value); var attributeValue = Assert.IsType<string>(attribute.Value);
Assert.Equal(url, attributeValue, StringComparer.Ordinal);
Assert.False(attribute.Minimized);
}
public static TheoryData UnresolvableUrlHtmlStringData
{
get
{
// url
return new TheoryData<HtmlString>
{
{ new HtmlString("/home/index.html") },
{ new HtmlString("~ /home/index.html") },
{ new HtmlString("/home/index.html ~/second/wontresolve.html") },
{ new HtmlString("~\\home\\index.html") },
{ new HtmlString("~\\/home/index.html") },
};
}
}
[Theory]
[MemberData(nameof(UnresolvableUrlHtmlStringData))]
public void Process_DoesNotResolveNonTildeSlashValues_InHtmlString(HtmlString url)
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
tagName: "a",
attributes: new TagHelperAttributeList
{
{ "href", url }
},
getChildContentAsync: _ => Task.FromResult<TagHelperContent>(null));
var urlHelperMock = new Mock<IUrlHelper>();
urlHelperMock
.Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
.Returns("approot/home/index.html");
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelperMock.Object);
var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
var context = new TagHelperContext(
allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
Enumerable.Empty<IReadOnlyTagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act
tagHelper.Process(context, tagHelperOutput);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
var attributeValue = Assert.IsType<HtmlString>(attribute.Value);
Assert.Equal(url.ToString(), attributeValue.ToString(), StringComparer.Ordinal);
Assert.False(attribute.Minimized); Assert.False(attribute.Minimized);
} }
@ -197,7 +297,7 @@ namespace Microsoft.AspNet.Mvc.Razor.TagHelpers
urlHelperFactory urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())) .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelperMock.Object); .Returns(urlHelperMock.Object);
var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, htmlEncoder: null); var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
var context = new TagHelperContext( var context = new TagHelperContext(
allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>( allAttributes: new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
// Arrange // Arrange
var resource = "window.alert('An alert');"; var resource = "window.alert('An alert');";
var expected = resource.Substring(0, resource.Length - 2);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource)); var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
var getManifestResourceStream = new Func<string, Stream>(name => stream); var getManifestResourceStream = new Func<string, Stream>(name => stream);
var cache = new ConcurrentDictionary<string, string>(); var cache = new ConcurrentDictionary<string, string>();
@ -24,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache); var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
// Assert // Assert
Assert.Equal(resource, result); Assert.Equal(expected, result);
} }
[Fact] [Fact]
@ -32,6 +33,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
// Arrange // Arrange
var resource = "window.alert('An alert');"; var resource = "window.alert('An alert');";
var expected = resource.Substring(0, resource.Length - 2);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource)); var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
var getManifestResourceStream = new Func<string, Stream>(name => stream); var getManifestResourceStream = new Func<string, Stream>(name => stream);
var cache = new ConcurrentDictionary<string, string>(); var cache = new ConcurrentDictionary<string, string>();
@ -43,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
Assert.Collection(cache, kvp => Assert.Collection(cache, kvp =>
{ {
Assert.Equal("test.js", kvp.Key); Assert.Equal("test.js", kvp.Key);
Assert.Equal(resource, kvp.Value); Assert.Equal(expected, kvp.Value);
}); });
} }
@ -70,12 +72,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
} }
[Theory] [Theory]
[InlineData("window.alert(\"[[[0]]]\")", "window.alert(\"{0}\")")] [InlineData("window.alert(\"[[[0]]]\")")]
[InlineData("var test = { a: 1 };", "var test = {{ a: 1 }};")] [InlineData("var test = { a: 1 };")]
[InlineData("var test = { a: 1, b: \"[[[0]]]\" };", "var test = {{ a: 1, b: \"{0}\" }};")] [InlineData("var test = { a: 1, b: \"[[[0]]]\" };")]
public void GetEmbeddedJavaScript_PreparesJavaScriptCorrectly(string resource, string expectedResult) public void GetEmbeddedJavaScript_PreparesJavaScriptCorrectly(string resource)
{ {
// Arrange // Arrange
var expected = resource.Substring(0, resource.Length - 2);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource)); var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
var getManifestResourceStream = new Func<string, Stream>(name => stream); var getManifestResourceStream = new Func<string, Stream>(name => stream);
var cache = new ConcurrentDictionary<string, string>(); var cache = new ConcurrentDictionary<string, string>();
@ -84,7 +87,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache); var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
// Assert // Assert
Assert.Equal(expectedResult, result); Assert.Equal(expected, result);
} }
} }
} }