// 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.Diagnostics; using System.IO; using System.Linq; 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.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; 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 RazorPage : IRazorPage { private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly Stack _tagHelperScopes = new Stack(); private IUrlHelper _urlHelper; private ITagHelperActivator _tagHelperActivator; private ITypeActivatorCache _typeActivatorCache; private bool _renderedBody; private AttributeInfo _attributeInfo; private TagHelperAttributeInfo _tagHelperAttributeInfo; private HtmlContentWrapperTextWriter _valueBuffer; private IViewBufferScope _bufferScope; public RazorPage() { SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// An representing the current request execution. /// public HttpContext Context => ViewContext?.HttpContext; /// public string Path { get; set; } /// public ViewContext ViewContext { get; set; } /// public string Layout { get; set; } /// /// Gets the to use when this /// handles non- C# expressions. /// [RazorInject] public HtmlEncoder HtmlEncoder { get; set; } /// /// Gets or sets a instance used to instrument the page execution. /// [RazorInject] public DiagnosticSource DiagnosticSource { get; set; } /// /// 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; } } /// /// Gets the of the current logged in user. /// public virtual ClaimsPrincipal User => Context?.User; /// /// Gets the dynamic view data dictionary. /// public dynamic ViewBag => ViewContext?.ViewBag; /// /// Gets the from the . /// /// Returns null if is null. public ITempDataDictionary TempData => ViewContext?.TempData; /// public IHtmlContent BodyContent { get; set; } /// public bool IsLayoutBeingRendered { get; set; } /// public IDictionary PreviousSectionWriters { get; set; } /// public IDictionary SectionWriters { get; } /// public abstract Task ExecuteAsync(); private ITagHelperActivator TagHelperActivator { get { if (_tagHelperActivator == null) { var services = ViewContext.HttpContext.RequestServices; _tagHelperActivator = services.GetRequiredService(); } return _tagHelperActivator; } } private ITypeActivatorCache TypeActivatorCache { get { if (_typeActivatorCache == null) { var services = ViewContext.HttpContext.RequestServices; _typeActivatorCache = services.GetRequiredService(); } return _typeActivatorCache; } } private IViewBufferScope BufferScope { get { if (_bufferScope == null) { var services = ViewContext.HttpContext.RequestServices; _bufferScope = services.GetRequiredService(); } return _bufferScope; } } /// /// 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. public static 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 { var tagHelper = TypeActivatorCache.CreateInstance( ViewContext.HttpContext.RequestServices, typeof(TTagHelper)); TagHelperActivator.Activate(tagHelper, ViewContext); return tagHelper; } /// /// 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) { _tagHelperScopes.Push(new TagHelperScopeInfo(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. var buffer = new ViewBuffer(BufferScope, Path); var writer = new HtmlContentWrapperTextWriter(buffer, ViewContext.Writer.Encoding); ViewContext.Writer = writer; } /// /// 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); } // Get the content written during the current scope. var writer = ViewContext.Writer as HtmlContentWrapperTextWriter; var tagHelperContent = new DefaultTagHelperContent(); tagHelperContent.AppendHtml(writer?.ContentBuilder); // Restore previous scope. var scopeInfo = _tagHelperScopes.Pop(); HtmlEncoder = scopeInfo.Encoder; ViewContext.Writer = scopeInfo.Writer; return tagHelperContent; } /// /// Writes the specified with HTML encoding to . /// /// The to write. public virtual void Write(object value) { WriteTo(Output, value); } /// /// Writes the specified with HTML encoding to . /// /// The instance to write to. /// The to write. /// /// s of type are written using /// . /// For all other types, the encoded result of is written to the /// . /// public virtual void WriteTo(TextWriter writer, object value) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } WriteTo(writer, HtmlEncoder, value); } /// /// Writes the specified with HTML encoding to given . /// /// The instance to write to. /// /// The to use when encoding . /// /// The to write. /// /// s of type are written using /// . /// For all other types, the encoded result of is written to the /// . /// public static void WriteTo(TextWriter writer, HtmlEncoder encoder, object value) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (encoder == null) { throw new ArgumentNullException(nameof(encoder)); } if (value == null || value == HtmlString.Empty) { return; } var htmlContent = value as IHtmlContent; if (htmlContent != null) { var htmlTextWriter = writer as HtmlTextWriter; if (htmlTextWriter == null) { htmlContent.WriteTo(writer, encoder); } else { // This special case allows us to keep buffering as IHtmlContent until we get to the 'final' // TextWriter. htmlTextWriter.Write(htmlContent); } return; } WriteTo(writer, encoder, value.ToString()); } /// /// Writes the specified with HTML encoding to . /// /// The instance to write to. /// The to write. public virtual void WriteTo(TextWriter writer, string value) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } WriteTo(writer, HtmlEncoder, value); } private static void WriteTo(TextWriter writer, HtmlEncoder encoder, string value) { if (!string.IsNullOrEmpty(value)) { encoder.Encode(writer, value); } } /// /// Writes the specified without HTML encoding to . /// /// The to write. public virtual void WriteLiteral(object value) { WriteLiteralTo(Output, value); } /// /// Writes the specified without HTML encoding to the . /// /// The instance to write to. /// The to write. public virtual void WriteLiteralTo(TextWriter writer, object value) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (value != null) { WriteLiteralTo(writer, value.ToString()); } } /// /// Writes the specified without HTML encoding to . /// /// The instance to write to. /// The to write. public virtual void WriteLiteralTo(TextWriter writer, string value) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (!string.IsNullOrEmpty(value)) { writer.Write(value); } } public virtual void BeginWriteAttribute( string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount) { BeginWriteAttributeTo(Output, name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); } public virtual void BeginWriteAttributeTo( TextWriter writer, string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } 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(writer, prefix, prefixOffset); } } public void WriteAttributeValue( string prefix, int prefixOffset, object value, int valueOffset, int valueLength, bool isLiteral) { WriteAttributeValueTo(Output, prefix, prefixOffset, value, valueOffset, valueLength, isLiteral); } public void WriteAttributeValueTo( TextWriter writer, 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(writer, _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(writer, prefix, prefixOffset); } BeginContext(valueOffset, valueLength, isLiteral); WriteUnprefixedAttributeValueTo(writer, value, isLiteral); EndContext(); } } public virtual void EndWriteAttribute() { EndWriteAttributeTo(Output); } public virtual void EndWriteAttributeTo(TextWriter writer) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (!_attributeInfo.Suppressed) { WritePositionTaggedLiteral(writer, _attributeInfo.Suffix, _attributeInfo.SuffixOffset); } } public void BeginAddHtmlAttributeValues( TagHelperExecutionContext executionContext, string attributeName, int attributeValuesCount) { _tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount); _valueBuffer = null; } 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.Suppressed = true; return; } else if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) { _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( _tagHelperAttributeInfo.Name, _tagHelperAttributeInfo.Name); _tagHelperAttributeInfo.Suppressed = true; return; } } if (value != null) { if (_valueBuffer == null) { var buffer = new ViewBuffer(BufferScope, Path); _valueBuffer = new HtmlContentWrapperTextWriter(buffer, Output.Encoding); } if (!string.IsNullOrEmpty(prefix)) { WriteLiteralTo(_valueBuffer, prefix); } WriteUnprefixedAttributeValueTo(_valueBuffer, value, isLiteral); } } public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext) { if (!_tagHelperAttributeInfo.Suppressed) { executionContext.AddHtmlAttribute( _tagHelperAttributeInfo.Name, (IHtmlContent)_valueBuffer?.ContentBuilder ?? HtmlString.Empty); _valueBuffer = null; } } public virtual string Href(string contentPath) { if (contentPath == null) { throw new ArgumentNullException(nameof(contentPath)); } if (_urlHelper == null) { var services = Context.RequestServices; var factory = services.GetRequiredService(); _urlHelper = factory.GetUrlHelper(ViewContext); } return _urlHelper.Content(contentPath); } private void WriteUnprefixedAttributeValueTo(TextWriter writer, 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) { WriteLiteralTo(writer, stringValue); } else if (isLiteral) { WriteLiteralTo(writer, value); } else if (stringValue != null) { WriteTo(writer, stringValue); } else { WriteTo(writer, value); } } private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) { BeginContext(position, value.Length, isLiteral: true); WriteLiteralTo(writer, value); EndContext(); } /// /// In a Razor layout page, renders the portion of a content page that is not within a named section. /// /// The HTML content to render. protected virtual IHtmlContent RenderBody() { if (BodyContent == null) { var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path); throw new InvalidOperationException(message); } _renderedBody = true; return BodyContent; } /// /// Creates a named content section in the page that can be invoked in a Layout page using /// or . /// /// The name of the section to create. /// The to execute when rendering the section. public 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; } /// /// Returns a value that indicates whether the specified section is defined in the content page. /// /// The section name to search for. /// true if the specified section is defined in the content page; otherwise, false. public bool IsSectionDefined(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } EnsureMethodCanBeInvoked(nameof(IsSectionDefined)); return PreviousSectionWriters.ContainsKey(name); } /// /// In layout pages, renders the content of the section named . /// /// The name of the section to render. /// Returns to allow the call to /// succeed. /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public HtmlString RenderSection(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } return RenderSection(name, required: true); } /// /// In layout pages, renders the content of the section named . /// /// The section to render. /// Indicates if this section must be rendered. /// Returns to allow the call to /// succeed. /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public HtmlString RenderSection(string name, bool required) { if (name == null) { throw new ArgumentNullException(nameof(name)); } EnsureMethodCanBeInvoked(nameof(RenderSection)); var task = RenderSectionAsyncCore(name, required); return task.GetAwaiter().GetResult(); } /// /// In layout pages, asynchronously renders the content of the section named . /// /// The section to render. /// A that on completion returns that /// allows the call to succeed. /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public Task RenderSectionAsync(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } return RenderSectionAsync(name, required: true); } /// /// In layout pages, asynchronously renders the content of the section named . /// /// The section to render. /// A that on completion returns that /// allows the call to succeed. /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. /// if is true and the section /// was not registered using the @section in the Razor page. public Task RenderSectionAsync(string name, bool required) { if (name == null) { throw new ArgumentNullException(nameof(name)); } EnsureMethodCanBeInvoked(nameof(RenderSectionAsync)); return RenderSectionAsyncCore(name, required); } private async Task RenderSectionAsyncCore(string sectionName, bool required) { if (_renderedSections.Contains(sectionName)) { var message = Resources.FormatSectionAlreadyRendered(nameof(RenderSectionAsync), Path, sectionName); throw new InvalidOperationException(message); } RenderAsyncDelegate renderDelegate; if (PreviousSectionWriters.TryGetValue(sectionName, out renderDelegate)) { _renderedSections.Add(sectionName); await renderDelegate(Output); // Return a token value that allows the Write call that wraps the RenderSection \ RenderSectionAsync // to succeed. return HtmlString.Empty; } else if (required) { // If the section is not found, and it is not optional, throw an error. var message = Resources.FormatSectionNotDefined( ViewContext.ExecutingFilePath, sectionName, ViewContext.View.Path); throw new InvalidOperationException(message); } else { // If the section is optional and not found, then don't do anything. return null; } } /// /// 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 . /// 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 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 Context.Response.Body.FlushAsync(); return HtmlString.Empty; } /// public void EnsureRenderedBodyOrSections() { // a) all sections defined for this page are rendered. // b) if no sections are defined, then the body is rendered if it's available. if (PreviousSectionWriters != null && PreviousSectionWriters.Count > 0) { var sectionsNotRendered = PreviousSectionWriters.Keys.Except( _renderedSections, StringComparer.OrdinalIgnoreCase); if (sectionsNotRendered.Any()) { var sectionNames = string.Join(", ", sectionsNotRendered); throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames)); } } else if (BodyContent != null && !_renderedBody) { // There are no sections defined, but RenderBody was NOT called. // If a body was defined, then RenderBody should have been called. var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path); throw new InvalidOperationException(message); } } public void BeginContext(int position, int length, bool isLiteral) { const string BeginContextEvent = "Microsoft.AspNetCore.Mvc.Razor.BeginInstrumentationContext"; if (DiagnosticSource?.IsEnabled(BeginContextEvent) == true) { DiagnosticSource.Write( BeginContextEvent, new { httpContext = Context, path = Path, position = position, length = length, isLiteral = isLiteral, }); } } public void EndContext() { const string EndContextEvent = "Microsoft.AspNetCore.Mvc.Razor.EndInstrumentationContext"; if (DiagnosticSource?.IsEnabled(EndContextEvent) == true) { DiagnosticSource.Write( EndContextEvent, new { httpContext = Context, path = Path, }); } } /// /// Sets antiforgery cookie and X-Frame-Options header on the response. /// /// . /// 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 = Context.RequestServices.GetRequiredService(); antiforgery.SetCookieTokenAndHeader(Context); return HtmlString.Empty; } 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); } private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) { throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName, Path)); } } 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) { ExecutionContext = tagHelperExecutionContext; Name = name; AttributeValuesCount = attributeValuesCount; Suppressed = false; } public string Name { get; } public TagHelperExecutionContext ExecutionContext { get; } public int AttributeValuesCount { get; } public bool Suppressed { get; set; } } private struct TagHelperScopeInfo { public TagHelperScopeInfo(HtmlEncoder encoder, TextWriter writer) { Encoder = encoder; Writer = writer; } public HtmlEncoder Encoder { get; } public TextWriter Writer { get; } } } }