// 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.Diagnostics; using System.Globalization; using System.IO; using System.Text; using Microsoft.Extensions.WebEncoders; namespace Microsoft.AspNet.Html.Abstractions { /// /// Extension methods for . /// public static class HtmlContentBuilderExtensions { /// /// Appends the specified to the existing content after replacing each format /// item with the HTML encoded representation of the corresponding item in the /// array. /// /// /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed. /// /// /// The object array to format. Each element in the array will be formatted and then HTML encoded. /// /// A reference to this instance after the append operation has completed. public static IHtmlContentBuilder AppendFormat( this IHtmlContentBuilder builder, string format, params object[] args) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (format == null) { throw new ArgumentNullException(nameof(format)); } if (args == null) { throw new ArgumentNullException(nameof(args)); } builder.Append(new HtmlFormatString(format, args)); return builder; } /// /// Appends the specified to the existing content with information from the /// after replacing each format item with the HTML encoded /// representation of the corresponding item in the array. /// /// An object that supplies culture-specific formatting information. /// /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed. /// /// /// The object array to format. Each element in the array will be formatted and then HTML encoded. /// /// A reference to this instance after the append operation has completed. public static IHtmlContentBuilder AppendFormat( this IHtmlContentBuilder builder, IFormatProvider formatProvider, string format, params object[] args) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (format == null) { throw new ArgumentNullException(nameof(format)); } if (args == null) { throw new ArgumentNullException(nameof(args)); } builder.Append(new HtmlFormatString(formatProvider, format, args)); return builder; } /// /// Appends an . /// /// The . /// The . public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder) { builder.Append(HtmlEncodedString.NewLine); return builder; } /// /// Appends an after appending the value. /// The value is treated as unencoded as-provided, and will be HTML encoded before writing to output. /// /// The . /// The to append. /// The . public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, string unencoded) { builder.Append(unencoded); builder.Append(HtmlEncodedString.NewLine); return builder; } /// /// Appends an after appending the value. /// /// The . /// The to append. /// The . public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, IHtmlContent htmlContent) { builder.Append(htmlContent); builder.Append(HtmlEncodedString.NewLine); return builder; } /// /// Appends an after appending the value. /// The value is treated as HTML encoded as-provided, and no further encoding will be performed. /// /// The . /// The HTML encoded to append. /// The . public static IHtmlContentBuilder AppendLineEncoded(this IHtmlContentBuilder builder, string encoded) { builder.AppendEncoded(encoded); builder.Append(HtmlEncodedString.NewLine); return builder; } /// /// Sets the content to the value. The value is treated as unencoded as-provided, /// and will be HTML encoded before writing to output. /// /// The . /// The value that replaces the content. /// The . public static IHtmlContentBuilder SetContent(this IHtmlContentBuilder builder, string unencoded) { builder.Clear(); builder.Append(unencoded); return builder; } /// /// Sets the content to the value. /// /// The . /// The value that replaces the content. /// The . public static IHtmlContentBuilder SetContent(this IHtmlContentBuilder builder, IHtmlContent content) { builder.Clear(); builder.Append(content); return builder; } /// /// Sets the content to the value. The value is treated as HTML encoded as-provided, and /// no further encoding will be performed. /// /// The . /// The HTML encoded that replaces the content. /// The . public static IHtmlContentBuilder SetContentEncoded(this IHtmlContentBuilder builder, string encoded) { builder.Clear(); builder.AppendEncoded(encoded); return builder; } [DebuggerDisplay("{DebuggerToString()}")] private class HtmlEncodedString : IHtmlContent { public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine); private readonly string _value; public HtmlEncodedString(string value) { _value = value; } public void WriteTo(TextWriter writer, IHtmlEncoder encoder) { writer.Write(_value); } private string DebuggerToString() { using (var writer = new StringWriter()) { WriteTo(writer, HtmlEncoder.Default); return writer.ToString(); } } } [DebuggerDisplay("{DebuggerToString()}")] private class HtmlFormatString : IHtmlContent { private readonly IFormatProvider _formatProvider; private readonly string _format; private readonly object[] _args; public HtmlFormatString(string format, object[] args) : this(null, format, args) { } public HtmlFormatString(IFormatProvider formatProvider, string format, object[] args) { Debug.Assert(format != null); Debug.Assert(args != null); _formatProvider = formatProvider ?? CultureInfo.CurrentCulture; _format = format; _args = args; } public void WriteTo(TextWriter writer, IHtmlEncoder encoder) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (encoder == null) { throw new ArgumentNullException(nameof(encoder)); } var formatProvider = new EncodingFormatProvider(_formatProvider, encoder); writer.Write(string.Format(formatProvider, _format, _args)); } private string DebuggerToString() { using (var writer = new StringWriter()) { WriteTo(writer, HtmlEncoder.Default); return writer.ToString(); } } } // This class implements Html encoding via an ICustomFormatter. Passing an instance of this // class into a string.Format method or anything similar will evaluate arguments implementing // IHtmlContent without HTML encoding them, and will give other arguments the standard // composite format string treatment, and then HTML encode the result. // // Plenty of examples of ICustomFormatter and the interactions with string.Format here: // https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx#Format6_Example private class EncodingFormatProvider : IFormatProvider, ICustomFormatter { private readonly IHtmlEncoder _encoder; private readonly IFormatProvider _formatProvider; public EncodingFormatProvider(IFormatProvider formatProvider, IHtmlEncoder encoder) { Debug.Assert(formatProvider != null); Debug.Assert(encoder != null); _formatProvider = formatProvider; _encoder = encoder; } public string Format(string format, object arg, IFormatProvider formatProvider) { // This is the case we need to special case. We trust the IHtmlContent instance to do the // right thing with encoding. var htmlContent = arg as IHtmlContent; if (htmlContent != null) { using (var writer = new StringWriter()) { htmlContent.WriteTo(writer, _encoder); return writer.ToString(); } } // If we get here then 'arg' is not an IHtmlContent, and we want to handle it the way a normal // string.Format would work, but then HTML encode the result. // // First check for an ICustomFormatter - if the IFormatProvider is a CultureInfo, then it's likely // that ICustomFormatter will be null. var customFormatter = (ICustomFormatter)_formatProvider.GetFormat(typeof(ICustomFormatter)); if (customFormatter != null) { var result = customFormatter.Format(format, arg, _formatProvider); if (result != null) { return _encoder.HtmlEncode(result); } } // Next check if 'arg' is an IFormattable (DateTime is an example). // // An IFormattable will likely call back into the IFormatterProvider and ask for more information // about how to format itself. This is the typical case when IFormatterProvider is a CultureInfo. var formattable = arg as IFormattable; if (formattable != null) { var result = formattable.ToString(format, _formatProvider); if (result != null) { return _encoder.HtmlEncode(result); } } // If we get here then there's nothing really smart left to try. if (arg != null) { var result = arg.ToString(); if (result != null) { return _encoder.HtmlEncode(result); } } return string.Empty; } public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) { return this; } return null; } } } }