// 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.Diagnostics; using System.IO; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.Razor { /// /// Represents properties and methods that are needed in order to render a view that uses Razor syntax. /// public abstract class RazorPageBase : IRazorPage { private readonly Stack _textWriterStack = new Stack(); private StringWriter _valueBuffer; private ITagHelperFactory _tagHelperFactory; private IViewBufferScope _bufferScope; private TextWriter _pageWriter; private AttributeInfo _attributeInfo; private TagHelperAttributeInfo _tagHelperAttributeInfo; private IUrlHelper _urlHelper; public virtual ViewContext ViewContext { get; set; } public string Layout { get; set; } /// /// Gets the that the page is writing output to. /// /// /// Gets the that the page is writing output to. /// public virtual TextWriter Output { get { if (ViewContext == null) { var message = Resources.FormatViewContextMustBeSet("ViewContext", "Output"); throw new InvalidOperationException(message); } return ViewContext.Writer; } } /// public string Path { get; set; } /// public IDictionary SectionWriters { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// Gets the dynamic view data dictionary. /// public dynamic ViewBag => ViewContext?.ViewBag; /// public bool IsLayoutBeingRendered { get; set; } /// public IHtmlContent BodyContent { get; set; } /// public IDictionary PreviousSectionWriters { get; set; } /// /// Gets or sets a instance used to instrument the page execution. /// [RazorInject] public DiagnosticSource DiagnosticSource { get; set; } /// /// Gets the to use when this /// handles non- C# expressions. /// [RazorInject] public HtmlEncoder HtmlEncoder { get; set; } /// /// Gets the of the current logged in user. /// public virtual ClaimsPrincipal User => ViewContext?.HttpContext?.User; /// /// Gets the from the . /// /// Returns null if is null. public ITempDataDictionary TempData => ViewContext?.TempData; private Stack TagHelperScopes { get; } = new Stack(); private ITagHelperFactory TagHelperFactory { get { if (_tagHelperFactory == null) { var services = ViewContext.HttpContext.RequestServices; _tagHelperFactory = services.GetRequiredService(); } return _tagHelperFactory; } } private IViewBufferScope BufferScope { get { if (_bufferScope == null) { var services = ViewContext.HttpContext.RequestServices; _bufferScope = services.GetRequiredService(); } return _bufferScope; } } public abstract Task ExecuteAsync(); /// /// Format an error message about using an indexer when the tag helper property is null. /// /// Name of the HTML attribute associated with the indexer. /// Full name of the tag helper . /// Dictionary property in the tag helper. /// An error message about using an indexer when the tag helper property is null. [EditorBrowsable(EditorBrowsableState.Never)] public string InvalidTagHelperIndexerAssignment( string attributeName, string tagHelperTypeName, string propertyName) { return Resources.FormatRazorPage_InvalidTagHelperIndexerAssignment( attributeName, tagHelperTypeName, propertyName); } /// /// Creates and activates a . /// /// A type. /// The activated . /// /// must have a parameterless constructor. /// public TTagHelper CreateTagHelper() where TTagHelper : ITagHelper { return TagHelperFactory.CreateTagHelper(ViewContext); } /// /// Starts a new writing scope and optionally overrides within that scope. /// /// /// The to use when this handles /// non- C# expressions. If null, does not change . /// /// /// All writes to the or after calling this method will /// be buffered until is called. /// public void StartTagHelperWritingScope(HtmlEncoder encoder) { var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize); TagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, ViewContext.Writer)); // If passed an HtmlEncoder, override the property. if (encoder != null) { HtmlEncoder = encoder; } // We need to replace the ViewContext's Writer to ensure that all content (including content written // from HTML helpers) is redirected. ViewContext.Writer = new ViewBufferTextWriter(buffer, ViewContext.Writer.Encoding); } /// /// Ends the current writing scope that was started by calling . /// /// The buffered . public TagHelperContent EndTagHelperWritingScope() { if (TagHelperScopes.Count == 0) { throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd); } var scopeInfo = TagHelperScopes.Pop(); // Get the content written during the current scope. var tagHelperContent = new DefaultTagHelperContent(); tagHelperContent.AppendHtml(scopeInfo.Buffer); // Restore previous scope. HtmlEncoder = scopeInfo.HtmlEncoder; ViewContext.Writer = scopeInfo.Writer; return tagHelperContent; } /// /// Starts a new scope for writing attribute values. /// /// /// 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 /// Nesting of and method calls /// is not supported. /// public void BeginWriteTagHelperAttribute() { if (_pageWriter != null) { throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported); } _pageWriter = ViewContext.Writer; if (_valueBuffer == null) { _valueBuffer = new StringWriter(); } // We need to replace the ViewContext's Writer to ensure that all content (including content written // from HTML helpers) is redirected. ViewContext.Writer = _valueBuffer; } /// /// Ends the current writing scope that was started by calling . /// /// The content buffered by the shared of this . /// /// This method assumes that there will be no nesting of /// and method calls. /// public string EndWriteTagHelperAttribute() { if (_pageWriter == null) { throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd); } var content = _valueBuffer.ToString(); _valueBuffer.GetStringBuilder().Clear(); // Restore previous writer. ViewContext.Writer = _pageWriter; _pageWriter = null; return content; } // Internal for unit testing. protected internal virtual void PushWriter(TextWriter writer) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } _textWriterStack.Push(ViewContext.Writer); ViewContext.Writer = writer; } // Internal for unit testing. protected internal virtual TextWriter PopWriter() { ViewContext.Writer = _textWriterStack.Pop(); return ViewContext.Writer; } public virtual string Href(string contentPath) { if (contentPath == null) { throw new ArgumentNullException(nameof(contentPath)); } if (_urlHelper == null) { var services = ViewContext?.HttpContext.RequestServices; var factory = services.GetRequiredService(); _urlHelper = factory.GetUrlHelper(ViewContext); } return _urlHelper.Content(contentPath); } /// /// Creates a named content section in the page that can be invoked in a Layout page using /// RenderSection or RenderSectionAsync /// /// The name of the section to create. /// The delegate to execute when rendering the section. /// This is a temporary placeholder method to support ASP.NET Core 2.0.0 editor code generation. [EditorBrowsable(EditorBrowsableState.Never)] protected void DefineSection(string name, Func section) => DefineSection(name, () => section(null /* writer */)); /// /// Creates a named content section in the page that can be invoked in a Layout page using /// RenderSection or RenderSectionAsync /// /// The name of the section to create. /// The to execute when rendering the section. public virtual void DefineSection(string name, RenderAsyncDelegate section) { if (name == null) { throw new ArgumentNullException(nameof(name)); } if (section == null) { throw new ArgumentNullException(nameof(section)); } if (SectionWriters.ContainsKey(name)) { throw new InvalidOperationException(Resources.FormatSectionAlreadyDefined(name)); } SectionWriters[name] = section; } /// /// Writes the specified with HTML encoding to . /// /// The to write. public virtual void Write(object value) { if (value == null || value == HtmlString.Empty) { return; } var writer = Output; var encoder = HtmlEncoder; if (value is IHtmlContent htmlContent) { var bufferedWriter = writer as ViewBufferTextWriter; if (bufferedWriter == null || !bufferedWriter.IsBuffering) { htmlContent.WriteTo(writer, encoder); } else { if (value is IHtmlContentContainer htmlContentContainer) { // This is likely another ViewBuffer. htmlContentContainer.MoveTo(bufferedWriter.Buffer); } else { // Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient // for writing character by character. bufferedWriter.Buffer.AppendHtml(htmlContent); } } return; } Write(value.ToString()); } /// /// Writes the specified with HTML encoding to . /// /// The to write. public virtual void Write(string value) { var writer = Output; var encoder = HtmlEncoder; if (!string.IsNullOrEmpty(value)) { // Perf: Encode right away instead of writing it character-by-character. // character-by-character isn't efficient when using a writer backed by a ViewBuffer. var encoded = encoder.Encode(value); writer.Write(encoded); } } /// /// Writes the specified without HTML encoding to . /// /// The to write. public virtual void WriteLiteral(object value) { if (value == null) { return; } WriteLiteral(value.ToString()); } /// /// Writes the specified without HTML encoding to . /// /// The to write. public virtual void WriteLiteral(string value) { if (!string.IsNullOrEmpty(value)) { Output.Write(value); } } public virtual void BeginWriteAttribute( string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount) { if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } if (suffix == null) { throw new ArgumentNullException(nameof(suffix)); } _attributeInfo = new AttributeInfo(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); // Single valued attributes might be omitted in entirety if it the attribute value strictly evaluates to // null or false. Consequently defer the prefix generation until we encounter the attribute value. if (attributeValuesCount != 1) { WritePositionTaggedLiteral(prefix, prefixOffset); } } public void WriteAttributeValue( string prefix, int prefixOffset, object value, int valueOffset, int valueLength, bool isLiteral) { if (_attributeInfo.AttributeValuesCount == 1) { if (IsBoolFalseOrNullValue(prefix, value)) { // Value is either null or the bool 'false' with no prefix; don't render the attribute. _attributeInfo.Suppressed = true; return; } // We are not omitting the attribute. Write the prefix. WritePositionTaggedLiteral(_attributeInfo.Prefix, _attributeInfo.PrefixOffset); if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) { // The value is just the bool 'true', write the attribute name instead of the string 'True'. value = _attributeInfo.Name; } } // This block handles two cases. // 1. Single value with prefix. // 2. Multiple values with or without prefix. if (value != null) { if (!string.IsNullOrEmpty(prefix)) { WritePositionTaggedLiteral(prefix, prefixOffset); } BeginContext(valueOffset, valueLength, isLiteral); WriteUnprefixedAttributeValue(value, isLiteral); EndContext(); } } public virtual void EndWriteAttribute() { if (!_attributeInfo.Suppressed) { WritePositionTaggedLiteral(_attributeInfo.Suffix, _attributeInfo.SuffixOffset); } } public void BeginAddHtmlAttributeValues( TagHelperExecutionContext executionContext, string attributeName, int attributeValuesCount, HtmlAttributeValueStyle attributeValueStyle) { _tagHelperAttributeInfo = new TagHelperAttributeInfo( executionContext, attributeName, attributeValuesCount, attributeValueStyle); } public void AddHtmlAttributeValue( string prefix, int prefixOffset, object value, int valueOffset, int valueLength, bool isLiteral) { Debug.Assert(_tagHelperAttributeInfo.ExecutionContext != null); if (_tagHelperAttributeInfo.AttributeValuesCount == 1) { if (IsBoolFalseOrNullValue(prefix, value)) { // The first value was 'null' or 'false' indicating that we shouldn't render the attribute. The // attribute is treated as a TagHelper attribute so it's only available in // TagHelperContext.AllAttributes for TagHelper authors to see (if they want to see why the // attribute was removed from TagHelperOutput.Attributes). _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute( _tagHelperAttributeInfo.Name, value?.ToString() ?? string.Empty, _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } else if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) { _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( _tagHelperAttributeInfo.Name, _tagHelperAttributeInfo.Name, _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } } if (value != null) { // Perf: We'll use this buffer for all of the attribute values and then clear it to // reduce allocations. if (_valueBuffer == null) { _valueBuffer = new StringWriter(); } PushWriter(_valueBuffer); if (!string.IsNullOrEmpty(prefix)) { WriteLiteral(prefix); } WriteUnprefixedAttributeValue(value, isLiteral); PopWriter(); } } public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext) { if (!_tagHelperAttributeInfo.Suppressed) { // Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so // it is reset for the next value. var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); _valueBuffer?.GetStringBuilder().Clear(); executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle); } } /// /// Invokes on and /// on the response stream, writing out any buffered content to the . /// /// A that represents the asynchronous flush operation and on /// completion returns an empty . /// 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. /// This method also writes out headers, so any modifications to headers must be done before /// is called. For example, call to send /// antiforgery cookie token and X-Frame-Options header to client before this method flushes headers out. /// public virtual async Task FlushAsync() { // If there are active scopes, then we should throw. Cannot flush content that has the potential to change. if (TagHelperScopes.Count > 0) { throw new InvalidOperationException( Resources.FormatRazorPage_CannotFlushWhileInAWritingScope(nameof(FlushAsync), Path)); } // Calls to Flush are allowed if the page does not specify a Layout or if it is executing a section in the // Layout. if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout)) { var message = Resources.FormatLayoutCannotBeRendered(Path, nameof(FlushAsync)); throw new InvalidOperationException(message); } await Output.FlushAsync(); await ViewContext?.HttpContext.Response.Body.FlushAsync(); return HtmlString.Empty; } /// /// Sets antiforgery cookie and X-Frame-Options header on the response. /// /// An empty . /// Call this method to send antiforgery cookie token and X-Frame-Options header to client /// before flushes the headers. public virtual HtmlString SetAntiforgeryCookieAndHeader() { var antiforgery = ViewContext?.HttpContext.RequestServices.GetRequiredService(); antiforgery.SetCookieTokenAndHeader(ViewContext?.HttpContext); return HtmlString.Empty; } private void WriteUnprefixedAttributeValue(object value, bool isLiteral) { var stringValue = value as string; // The extra branching here is to ensure that we call the Write*To(string) overload where possible. if (isLiteral && stringValue != null) { WriteLiteral(stringValue); } else if (isLiteral) { WriteLiteral(value); } else if (stringValue != null) { Write(stringValue); } else { Write(value); } } private void WritePositionTaggedLiteral(string value, int position) { BeginContext(position, value.Length, isLiteral: true); WriteLiteral(value); EndContext(); } public abstract void BeginContext(int position, int length, bool isLiteral); public abstract void EndContext(); private bool IsBoolFalseOrNullValue(string prefix, object value) { return string.IsNullOrEmpty(prefix) && (value == null || (value is bool && !(bool)value)); } private bool IsBoolTrueWithEmptyPrefixValue(string prefix, object value) { // If the value is just the bool 'true', use the attribute name as the value. return string.IsNullOrEmpty(prefix) && (value is bool && (bool)value); } public abstract void EnsureRenderedBodyOrSections(); private struct AttributeInfo { public AttributeInfo( string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount) { Name = name; Prefix = prefix; PrefixOffset = prefixOffset; Suffix = suffix; SuffixOffset = suffixOffset; AttributeValuesCount = attributeValuesCount; Suppressed = false; } public int AttributeValuesCount { get; } public string Name { get; } public string Prefix { get; } public int PrefixOffset { get; } public string Suffix { get; } public int SuffixOffset { get; } public bool Suppressed { get; set; } } private struct TagHelperAttributeInfo { public TagHelperAttributeInfo( TagHelperExecutionContext tagHelperExecutionContext, string name, int attributeValuesCount, HtmlAttributeValueStyle attributeValueStyle) { ExecutionContext = tagHelperExecutionContext; Name = name; AttributeValuesCount = attributeValuesCount; AttributeValueStyle = attributeValueStyle; Suppressed = false; } public string Name { get; } public TagHelperExecutionContext ExecutionContext { get; } public int AttributeValuesCount { get; } public HtmlAttributeValueStyle AttributeValueStyle { get; } public bool Suppressed { get; set; } } private struct TagHelperScopeInfo { public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer) { Buffer = buffer; HtmlEncoder = encoder; Writer = writer; } public ViewBuffer Buffer { get; } public HtmlEncoder HtmlEncoder { get; } public TextWriter Writer { get; } } } }