// 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.Globalization; using System.IO; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Antiforgery; using Microsoft.AspNet.Html.Abstractions; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Actions; using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; namespace Microsoft.AspNet.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 _writerScopes; private TextWriter _originalWriter; private IUrlHelper _urlHelper; private ITagHelperActivator _tagHelperActivator; private ITypeActivatorCache _typeActivatorCache; private bool _renderedBody; public RazorPage() { SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); _writerScopes = new Stack(); } public HttpContext Context { get { if (ViewContext == null) { return null; } return ViewContext.HttpContext; } } /// public string Path { get; set; } /// public ViewContext ViewContext { get; set; } /// public string Layout { get; set; } /// public bool IsPartial { get; set; } /// /// Gets the to be used for encoding HTML. /// [RazorInject] public IHtmlEncoder HtmlEncoder { get; set; } /// public IPageExecutionContext PageExecutionContext { get; set; } /// /// Gets the TextWriter 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 virtual ClaimsPrincipal User { get { if (Context == null) { return null; } return Context.User; } } public dynamic ViewBag { get { return ViewContext?.ViewBag; } } /// /// Gets the from the . /// /// Returns null if is null. public ITempDataDictionary TempData { get { return ViewContext?.TempData; } } /// public Func RenderBodyDelegateAsync { get; set; } /// public bool IsLayoutBeingRendered { get; set; } /// public IDictionary PreviousSectionWriters { get; set; } /// public IDictionary SectionWriters { get; private set; } /// 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; } } /// /// 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. /// /// /// All writes to the or after calling this method will /// be buffered until is called. /// public void StartTagHelperWritingScope() { StartTagHelperWritingScope(new StringCollectionTextWriter(Output.Encoding)); } /// /// Starts a new writing scope with the given . /// /// /// All writes to the or after calling this method will /// be buffered until is called. /// public void StartTagHelperWritingScope([NotNull] TextWriter writer) { // If there isn't a base writer take the ViewContext.Writer if (_originalWriter == null) { _originalWriter = ViewContext.Writer; } // We need to replace the ViewContext's Writer to ensure that all content (including content written // from HTML helpers) is redirected. ViewContext.Writer = writer; _writerScopes.Push(ViewContext.Writer); } /// /// Ends the current writing scope that was started by calling . /// /// The that contains the content written to the or /// during the writing scope. public TagHelperContent EndTagHelperWritingScope() { if (_writerScopes.Count == 0) { throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd); } var writer = _writerScopes.Pop(); if (_writerScopes.Count > 0) { ViewContext.Writer = _writerScopes.Peek(); } else { ViewContext.Writer = _originalWriter; // No longer a base writer _originalWriter = null; } var tagHelperContentWrapperTextWriter = new TagHelperContentWrapperTextWriter(Output.Encoding); var razorWriter = writer as RazorTextWriter; if (razorWriter != null) { razorWriter.CopyTo(tagHelperContentWrapperTextWriter); } else { var stringCollectionTextWriter = writer as StringCollectionTextWriter; if (stringCollectionTextWriter != null) { stringCollectionTextWriter.CopyTo(tagHelperContentWrapperTextWriter, HtmlEncoder); } else { tagHelperContentWrapperTextWriter.Write(writer.ToString()); } } return tagHelperContentWrapperTextWriter.Content; } /// /// Writes the content of a specified . /// /// The execution context containing the content. /// /// A that on completion writes the content. /// public Task WriteTagHelperAsync([NotNull] TagHelperExecutionContext tagHelperExecutionContext) { return WriteTagHelperToAsync(Output, tagHelperExecutionContext); } /// /// Writes the content of a specified to the specified /// . /// /// The instance to write to. /// The execution context containing the content. /// /// A that on completion writes the content /// to the . /// public async Task WriteTagHelperToAsync( [NotNull] TextWriter writer, [NotNull] TagHelperExecutionContext tagHelperExecutionContext) { var tagHelperOutput = tagHelperExecutionContext.Output; var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(tagHelperOutput.TagName); WriteTo(writer, tagHelperOutput.PreElement); if (!isTagNameNullOrWhitespace) { writer.Write('<'); writer.Write(tagHelperOutput.TagName); foreach (var attribute in tagHelperOutput.Attributes) { writer.Write(' '); writer.Write(attribute.Name); if (!attribute.Minimized) { writer.Write("=\""); WriteTo(writer, HtmlEncoder, attribute.Value, escapeQuotes: true); writer.Write('"'); } } if (tagHelperOutput.TagMode == TagMode.SelfClosing) { writer.Write(" /"); } writer.Write('>'); } if (isTagNameNullOrWhitespace || tagHelperOutput.TagMode == TagMode.StartTagAndEndTag) { WriteTo(writer, tagHelperOutput.PreContent); if (tagHelperOutput.IsContentModified) { WriteTo(writer, tagHelperOutput.Content); } else if (tagHelperExecutionContext.ChildContentRetrieved) { var childContent = await tagHelperExecutionContext.GetChildContentAsync(useCachedResult: true); WriteTo(writer, childContent); } else { await tagHelperExecutionContext.ExecuteChildContentAsync(); } WriteTo(writer, tagHelperOutput.PostContent); } if (!isTagNameNullOrWhitespace && tagHelperOutput.TagMode == TagMode.StartTagAndEndTag) { writer.Write(string.Format(CultureInfo.InvariantCulture, "", tagHelperOutput.TagName)); } WriteTo(writer, tagHelperOutput.PostElement); } /// /// 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([NotNull] TextWriter writer, object value) { WriteTo(writer, HtmlEncoder, value, escapeQuotes: false); } /// /// Writes the specified with HTML encoding to given . /// /// The instance to write to. /// The to use when encoding . /// The to write. /// /// If true escapes double quotes in a of type . /// Otherwise writes values as-is. /// /// /// s of type are written using /// . /// For all other types, the encoded result of is written to the /// . /// public static void WriteTo( [NotNull] TextWriter writer, [NotNull] IHtmlEncoder encoder, object value, bool escapeQuotes) { if (value == null || value == HtmlString.Empty) { return; } var htmlContent = value as IHtmlContent; if (htmlContent != null) { if (escapeQuotes) { // 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. // // This is really not optimal from a perf point of view, but it's the best we can do for right now. using (var stringWriter = new StringWriter()) { htmlContent.WriteTo(stringWriter, encoder); var stringValue = stringWriter.ToString(); if (stringValue.Contains("\"")) { stringValue = stringValue.Replace("\"", """); } writer.Write(stringValue); return; } } var htmlTextWriter = writer as HtmlTextWriter; if (htmlTextWriter == null) { htmlContent.WriteTo(writer, encoder); } else { // This special case alows 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([NotNull] TextWriter writer, string value) { WriteTo(writer, HtmlEncoder, value); } private static void WriteTo(TextWriter writer, IHtmlEncoder encoder, string value) { if (!string.IsNullOrEmpty(value)) { encoder.HtmlEncode(value, writer); } } /// /// 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([NotNull] TextWriter writer, object value) { 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([NotNull] TextWriter writer, string value) { if (!string.IsNullOrEmpty(value)) { writer.Write(value); } } public virtual void WriteAttribute( string name, [NotNull] PositionTagged prefix, [NotNull] PositionTagged suffix, params AttributeValue[] values) { WriteAttributeTo(Output, name, prefix, suffix, values); } public virtual void WriteAttributeTo( [NotNull] TextWriter writer, string name, [NotNull] PositionTagged prefix, [NotNull] PositionTagged suffix, params AttributeValue[] values) { if (values.Length == 0) { // Explicitly empty attribute, so write the prefix WritePositionTaggedLiteral(writer, prefix); } else if (IsSingleBoolFalseOrNullValue(values)) { // Value is either null or the bool 'false' with no prefix; don't render the attribute. return; } else if (UseAttributeNameAsValue(values)) { var attributeValue = values[0]; var positionTaggedAttributeValue = attributeValue.Value; WritePositionTaggedLiteral(writer, prefix); var sourceLength = suffix.Position - positionTaggedAttributeValue.Position; var nameAttributeValue = new AttributeValue( attributeValue.Prefix, new PositionTagged(name, attributeValue.Value.Position), literal: attributeValue.Literal); // The value is just the bool 'true', write the attribute name instead of the string 'True'. WriteAttributeValue(writer, nameAttributeValue, sourceLength); } else { // This block handles two cases. // 1. Single value with prefix. // 2. Multiple values with or without prefix. WritePositionTaggedLiteral(writer, prefix); for (var i = 0; i < values.Length; i++) { var attributeValue = values[i]; var positionTaggedAttributeValue = attributeValue.Value; if (positionTaggedAttributeValue.Value == null) { // Nothing to write continue; } var next = i == values.Length - 1 ? suffix : // End of the list, grab the suffix values[i + 1].Prefix; // Still in the list, grab the next prefix // Calculate length of the source span by the position of the next value (or suffix) var sourceLength = next.Position - attributeValue.Value.Position; WriteAttributeValue(writer, attributeValue, sourceLength); } } WritePositionTaggedLiteral(writer, suffix); } public void AddHtmlAttributeValues( string attributeName, TagHelperExecutionContext executionContext, params AttributeValue[] values) { if (IsSingleBoolFalseOrNullValue(values)) { // 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). executionContext.AddTagHelperAttribute( attributeName, values[0].Value.Value?.ToString() ?? string.Empty); return; } else if (UseAttributeNameAsValue(values)) { executionContext.AddHtmlAttribute(attributeName, attributeName); } else { var valueBuffer = new StringCollectionTextWriter(Output.Encoding); foreach (var value in values) { if (value.Value.Value == null) { // Skip null values continue; } if (!string.IsNullOrEmpty(value.Prefix)) { WriteLiteralTo(valueBuffer, value.Prefix); } WriteUnprefixedAttributeValueTo(valueBuffer, value); } using (var stringWriter = new StringWriter()) { valueBuffer.Content.WriteTo(stringWriter, HtmlEncoder); var htmlString = new HtmlString(stringWriter.ToString()); executionContext.AddHtmlAttribute(attributeName, htmlString); } } } public virtual string Href([NotNull] string contentPath) { if (_urlHelper == null) { _urlHelper = Context.RequestServices.GetRequiredService(); } return _urlHelper.Content(contentPath); } private void WriteAttributeValue(TextWriter writer, AttributeValue attributeValue, int sourceLength) { if (!string.IsNullOrEmpty(attributeValue.Prefix)) { WritePositionTaggedLiteral(writer, attributeValue.Prefix); } BeginContext(attributeValue.Value.Position, sourceLength, isLiteral: attributeValue.Literal); WriteUnprefixedAttributeValueTo(writer, attributeValue); EndContext(); } private void WriteUnprefixedAttributeValueTo(TextWriter writer, AttributeValue attributeValue) { var positionTaggedAttributeValue = attributeValue.Value; var stringValue = positionTaggedAttributeValue.Value as string; // The extra branching here is to ensure that we call the Write*To(string) overload where possible. if (attributeValue.Literal && stringValue != null) { WriteLiteralTo(writer, stringValue); } else if (attributeValue.Literal) { WriteLiteralTo(writer, positionTaggedAttributeValue.Value); } else if (stringValue != null) { WriteTo(writer, stringValue); } else { WriteTo(writer, positionTaggedAttributeValue.Value); } } private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) { BeginContext(position, value.Length, isLiteral: true); WriteLiteralTo(writer, value); EndContext(); } private void WritePositionTaggedLiteral(TextWriter writer, PositionTagged value) { WritePositionTaggedLiteral(writer, value.Value, value.Position); } protected virtual HelperResult RenderBody() { if (RenderBodyDelegateAsync == null) { var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path); throw new InvalidOperationException(message); } _renderedBody = true; return new HelperResult(RenderBodyDelegateAsync); } /// /// 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([NotNull] string name, [NotNull] RenderAsyncDelegate section) { if (SectionWriters.ContainsKey(name)) { throw new InvalidOperationException(Resources.FormatSectionAlreadyDefined(name)); } SectionWriters[name] = section; } public bool IsSectionDefined([NotNull] string 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([NotNull] string 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([NotNull] string name, bool required) { 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([NotNull] string 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([NotNull] string name, bool required) { 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. throw new InvalidOperationException(Resources.FormatSectionNotDefined(sectionName, Path)); } else { // If the section is optional and not found, then don't do anything. return null; } } /// /// Invokes on writing out any buffered /// content to the . /// /// A that represents the asynchronous flush operation and on /// completion returns a . /// 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 writing scopes then we should throw. Cannot flush content that has the potential to // change. if (_writerScopes.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(); 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 (RenderBodyDelegateAsync != 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) { PageExecutionContext?.BeginContext(position, length, isLiteral); } public void EndContext() { PageExecutionContext?.EndContext(); } /// /// Sets antiforgery cookie and X-Frame-Options header on the response. /// /// A that returns a . /// 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 IsSingleBoolFalseOrNullValue(AttributeValue[] values) { if (values.Length == 1 && string.IsNullOrEmpty(values[0].Prefix) && (values[0].Value.Value is bool || values[0].Value.Value == null)) { var attributeValue = values[0]; var positionTaggedAttributeValue = attributeValue.Value; if (positionTaggedAttributeValue.Value == null || !(bool)positionTaggedAttributeValue.Value) { return true; } } return false; } private bool UseAttributeNameAsValue(AttributeValue[] values) { // If the value is just the bool 'true', use the attribute name as the value. return values.Length == 1 && string.IsNullOrEmpty(values[0].Prefix) && values[0].Value.Value is bool && (bool)values[0].Value.Value; } private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) { throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName, Path)); } } } }