diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs index 4c97ecbea3..bb537aaa51 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs @@ -88,8 +88,6 @@ internal MvcRazorHost(IChunkTreeCache chunkTreeCache, RazorPathNormalizer pathNo ExecutionContextAddTagHelperAttributeMethodName = nameof(TagHelperExecutionContext.AddTagHelperAttribute), ExecutionContextAddHtmlAttributeMethodName = nameof(TagHelperExecutionContext.AddHtmlAttribute), - ExecutionContextAddMinimizedHtmlAttributeMethodName = - nameof(TagHelperExecutionContext.AddMinimizedHtmlAttribute), ExecutionContextOutputPropertyName = nameof(TagHelperExecutionContext.Output), RunnerTypeName = typeof(TagHelperRunner).FullName, diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 0e5581ee75..3fe6054600 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -238,7 +238,7 @@ public TagHelperContent EndTagHelperWritingScope() /// /// All writes to the or after calling this method will /// be buffered until is called. - /// The content will be buffered using a shared within this + /// The content will be buffered using a shared within this /// Nesting of and method calls /// is not supported. /// @@ -267,7 +267,7 @@ public void BeginWriteTagHelperAttribute() /// /// The content buffered by the shared of this . /// - /// This method assumes that there will be no nesting of + /// This method assumes that there will be no nesting of /// and method calls. /// public string EndWriteTagHelperAttribute() @@ -572,9 +572,14 @@ public virtual void EndWriteAttributeTo(TextWriter writer) public void BeginAddHtmlAttributeValues( TagHelperExecutionContext executionContext, string attributeName, - int attributeValuesCount) + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) { - _tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount); + _tagHelperAttributeInfo = new TagHelperAttributeInfo( + executionContext, + attributeName, + attributeValuesCount, + attributeValueStyle); } public void AddHtmlAttributeValue( @@ -596,7 +601,8 @@ public void AddHtmlAttributeValue( // attribute was removed from TagHelperOutput.Attributes). _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute( _tagHelperAttributeInfo.Name, - value?.ToString() ?? string.Empty); + value?.ToString() ?? string.Empty, + _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } @@ -604,7 +610,8 @@ public void AddHtmlAttributeValue( { _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( _tagHelperAttributeInfo.Name, - _tagHelperAttributeInfo.Name); + _tagHelperAttributeInfo.Name, + _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } @@ -637,7 +644,7 @@ public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); _valueBuffer?.GetStringBuilder().Clear(); - executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content); + executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle); } } @@ -1081,11 +1088,13 @@ private struct TagHelperAttributeInfo public TagHelperAttributeInfo( TagHelperExecutionContext tagHelperExecutionContext, string name, - int attributeValuesCount) + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) { ExecutionContext = tagHelperExecutionContext; Name = name; AttributeValuesCount = attributeValuesCount; + AttributeValueStyle = attributeValueStyle; Suppressed = false; } @@ -1096,6 +1105,8 @@ public TagHelperAttributeInfo( public int AttributeValuesCount { get; } + public HtmlAttributeValueStyle AttributeValueStyle { get; } + public bool Suppressed { get; set; } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs index 82a4bd40b3..2abec8a630 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs @@ -422,7 +422,8 @@ private void BuildLinkTag(string href, TagHelperAttributeList attributes, TagHel } else { - AppendAttribute(attribute.Name, attribute.Value, builder); + attribute.CopyTo(builder); + builder.AppendHtml(" "); } } @@ -441,15 +442,10 @@ private void AppendVersionedHref(string hrefName, string hrefValue, TagHelperCon hrefValue = _fileVersionProvider.AddFileVersionToPath(hrefValue); } - AppendAttribute(hrefName, hrefValue, builder); - } - - private void AppendAttribute(string key, object value, TagHelperContent builder) - { builder - .AppendHtml(key) + .AppendHtml(hrefName) .AppendHtml("=\"") - .Append(HtmlEncoder, value) + .Append(hrefValue) .AppendHtml("\" "); } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs index e821afd48a..afec945db1 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; @@ -235,7 +236,8 @@ public override void Process(TagHelperContext context, TagHelperOutput output) var index = output.Attributes.IndexOfName(SrcAttributeName); output.Attributes[index] = new TagHelperAttribute( SrcAttributeName, - _fileVersionProvider.AddFileVersionToPath(Src)); + _fileVersionProvider.AddFileVersionToPath(Src), + output.Attributes[index].ValueStyle); } } @@ -306,7 +308,7 @@ private void BuildFallbackBlock(TagHelperAttributeList attributes, TagHelperCont // Fallback "src" values come from bound attributes and globbing. Must always be non-null. Debug.Assert(src != null); - builder.AppendHtml("<\\/script>"); + StringWriter.Write(">"); } + var stringBuilder = StringWriter.GetStringBuilder(); + var scriptTags = stringBuilder.ToString(); + stringBuilder.Clear(); + var encodedScriptTags = JavaScriptEncoder.Encode(scriptTags); + builder.AppendHtml(encodedScriptTags); + builder.AppendHtml("\"));"); } } - private string GetAttributeValue(object value) + private string GetVersionedSrc(string srcValue) { - string stringValue; - var htmlString = value as HtmlString; - if (htmlString != null) + if (AppendVersion == true) { - // Value likely came from an HTML context in the .cshtml file but may still contain double quotes - // since attribute could have been enclosed in single quotes. - stringValue = htmlString.Value; - stringValue = stringValue.Replace("\"", """); + srcValue = _fileVersionProvider.AddFileVersionToPath(srcValue); } - else - { - var writer = StringWriter; - RazorPage.WriteTo(writer, HtmlEncoder, value); - // Value is now correctly HTML-encoded but may still contain double quotes since attribute could - // have been enclosed in single quotes and portions that were HtmlStrings are not re-encoded. - var builder = writer.GetStringBuilder(); - builder.Replace("\"", """); + return srcValue; + } - stringValue = builder.ToString(); - builder.Clear(); - } + private void AppendVersionedSrc( + string srcName, + string srcValue, + HtmlAttributeValueStyle valueStyle, + IHtmlContentBuilder builder) + { + srcValue = GetVersionedSrc(srcValue); - return stringValue; + builder.AppendHtml(" "); + var attribute = new TagHelperAttribute(srcName, srcValue, valueStyle); + attribute.CopyTo(builder); } - private void AppendEncodedVersionedSrc( + private void WriteVersionedSrc( string srcName, string srcValue, - TagHelperContent builder, - bool generateForDocumentWrite) + HtmlAttributeValueStyle valueStyle, + TextWriter writer) { - if (AppendVersion == true) - { - srcValue = _fileVersionProvider.AddFileVersionToPath(srcValue); - } - - if (generateForDocumentWrite) - { - // srcValue comes from a C# context and globbing. Must HTML-encode it to ensure the - // written "); } - private void AppendAttribute(TagHelperContent content, string key, object value, bool escapeQuotes) - { - content - .AppendHtml(" ") - .AppendHtml(key); - if (escapeQuotes) - { - // Passed only JavaScript-encoded strings in this case. Do not perform HTML-encoding as well. - content - .AppendHtml("=\\\"") - .AppendHtml((string)value) - .AppendHtml("\\\""); - } - else - { - // HTML-encode the given value if necessary. - content - .AppendHtml("=\"") - .Append(HtmlEncoder, value) - .AppendHtml("\""); - } - } - private enum Mode { /// diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs deleted file mode 100644 index cd6740bd8e..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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.IO; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Razor.TagHelpers; - -namespace Microsoft.AspNetCore.Mvc.TagHelpers -{ - /// - /// Extension methods for . - /// - public static class TagHelperContentExtensions - { - /// - /// Writes the specified with HTML encoding to given . - /// - /// The to write to. - /// The to use when encoding . - /// The to write. - /// after the write operation has completed. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of - /// is written to the . - /// - public static TagHelperContent Append(this TagHelperContent content, HtmlEncoder encoder, object value) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (value == null) - { - // No real action but touch content to ensure IsModified is true. - content.Append((string)null); - return 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()) - { - 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. - content.AppendHtml(stringValue.Replace("\"", """)); - - return content; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs index dcd917675b..89168a6497 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs @@ -158,10 +158,6 @@ private static void CopyHtmlAttribute( TagHelperContext context) { var existingAttribute = context.AllAttributes[allAttributeIndex]; - var copiedAttribute = new TagHelperAttribute( - existingAttribute.Name, - existingAttribute.Value, - existingAttribute.Minimized); // Move backwards through context.AllAttributes from the provided index until we find a familiar attribute // in tagHelperOutput where we can insert the copied value after the familiar one. @@ -171,7 +167,7 @@ private static void CopyHtmlAttribute( var index = IndexOfFirstMatch(previousName, tagHelperOutput.Attributes); if (index != -1) { - tagHelperOutput.Attributes.Insert(index + 1, copiedAttribute); + tagHelperOutput.Attributes.Insert(index + 1, existingAttribute); return; } } @@ -184,13 +180,13 @@ private static void CopyHtmlAttribute( var index = IndexOfFirstMatch(nextName, tagHelperOutput.Attributes); if (index != -1) { - tagHelperOutput.Attributes.Insert(index, copiedAttribute); + tagHelperOutput.Attributes.Insert(index, existingAttribute); return; } } // Couldn't determine the attribute's location, add it to the end. - tagHelperOutput.Attributes.Add(copiedAttribute); + tagHelperOutput.Attributes.Add(existingAttribute); } private static int IndexOfFirstMatch(string name, TagHelperAttributeList attributes) diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html index 5d696d4c25..39438a1bd7 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html @@ -4,7 +4,7 @@ Product Index
HtmlGenerationWebSite Index diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html index ea526da9a7..e80ec4969e 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html @@ -4,7 +4,7 @@ Product Index
HtmlGenerationWebSite Index diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html index c87ec590a7..a5dac00b74 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html index 0384f385c8..370692a400 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html index f696105479..680a5729a0 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html @@ -13,27 +13,27 @@

Script tag helper test

- +]]")); - - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - + - - + - + - + - + - + - + - + - + " + - ""; var mixed = new DefaultTagHelperContent(); mixed.Append("HTML encoded"); @@ -636,18 +636,18 @@ public void RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected() attributes: new TagHelperAttributeList { { "asp-src-include", "**/*.js" }, - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, { "src", "/js/site.js" }, }); var output = MakeTagHelperOutput( "script", attributes: new TagHelperAttributeList { - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded"}, - { "mixed", mixed}, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, }); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); @@ -786,9 +786,9 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion() // Assert Assert.Equal("script", output.TagName); Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); - Assert.Equal(Environment.NewLine + "", output.PostElement.GetContent()); + Assert.Equal(Environment.NewLine + "]]\"));", output.PostElement.GetContent()); } [Fact] @@ -796,16 +796,14 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() { // Arrange var expectedContent = - "" + Environment.NewLine + - ""; + "]]\"));"; var mixed = new DefaultTagHelperContent(); mixed.Append("HTML encoded"); mixed.AppendHtml(" and contains \"quotes\""); @@ -815,18 +813,18 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() { "asp-append-version", "true" }, { "asp-fallback-src-include", "fallback.js" }, { "asp-fallback-test", "isavailable()" }, - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, { "src", "/js/site.js" }, }); var output = MakeTagHelperOutput( "script", attributes: new TagHelperAttributeList { - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, }); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs index 0784d3f29c..eea6e30df0 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs @@ -979,13 +979,21 @@ public bool Equals(TagHelperAttribute attributeX, TagHelperAttribute attributeY) // Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do. return attributeX != null && string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) && - attributeX.Minimized == attributeY.Minimized && - (attributeX.Minimized || Equals(attributeX.Value, attributeY.Value)); + attributeX.ValueStyle == attributeY.ValueStyle && + (attributeX.ValueStyle == HtmlAttributeValueStyle.Minimized || Equals(attributeX.Value, attributeY.Value)); } public int GetHashCode(TagHelperAttribute attribute) { - return attribute.GetHashCode(); + // Manually combine hash codes here. We can't reference HashCodeCombiner because we have internals visible + // from Mvc.Core and Mvc.TagHelpers; both of which reference HashCodeCombiner. + var baseHashCode = 0x1505L; + var attributeHashCode = attribute.GetHashCode(); + var combinedHash = ((baseHashCode << 5) + baseHashCode) ^ attributeHashCode; + var nameHashCode = StringComparer.Ordinal.GetHashCode(attribute.Name); + combinedHash = ((combinedHash << 5) + combinedHash) ^ nameHashCode; + + return combinedHash.GetHashCode(); } } }