// Copyright (c) Microsoft Open Technologies, Inc. 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.IO;
using System.Linq;
using System.Net;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.DependencyInjection;
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 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; }
///
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 IPrincipal User
{
get
{
if (Context == null)
{
return null;
}
return Context.User;
}
}
public dynamic ViewBag
{
get
{
return (ViewContext == null) ? null : ViewContext.ViewBag;
}
}
///
public Action RenderBodyDelegate { 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)
{
_tagHelperActivator =
ViewContext.HttpContext.RequestServices.GetRequiredService();
}
return _tagHelperActivator;
}
}
///
/// Creates and activates a .
///
/// A type.
/// The activated .
///
/// must have a parameterless constructor.
///
public TTagHelper CreateTagHelper() where TTagHelper : ITagHelper, new()
{
var tagHelper = new 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 StartWritingScope()
{
StartWritingScope(new StringWriter());
}
///
/// 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 StartWritingScope(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 TextWriter EndWritingScope()
{
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;
}
return writer;
}
///
/// 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 without encoding and the
/// is invoked for types.
/// For all other types, the encoded result of is written to the
/// .
///
public virtual void WriteTo(TextWriter writer, object value)
{
if (value != null && value != HtmlString.Empty)
{
var helperResult = value as HelperResult;
if (helperResult != null)
{
helperResult.WriteTo(writer);
}
else
{
var htmlString = value as HtmlString;
if (htmlString != null)
{
writer.Write(htmlString);
}
else
{
WriteTo(writer, 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 (!string.IsNullOrEmpty(value))
{
writer.Write(WebUtility.HtmlEncode(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 (value != null)
{
WriteLiteralTo(writer, value.ToString());
}
}
///
/// Writes the specified without HTML encoding to .
///
/// The to write.
public virtual void WriteLiteralTo(TextWriter writer, string value)
{
if (!string.IsNullOrEmpty(value))
{
writer.Write(value);
}
}
public virtual void WriteAttribute(string name,
PositionTagged prefix,
PositionTagged suffix,
params AttributeValue[] values)
{
WriteAttributeTo(Output, name, prefix, suffix, values);
}
public virtual void WriteAttributeTo(TextWriter writer,
string name,
PositionTagged prefix,
PositionTagged suffix,
params AttributeValue[] values)
{
var first = true;
var wroteSomething = false;
if (values.Length == 0)
{
// Explicitly empty attribute, so write the prefix and suffix
WritePositionTaggedLiteral(writer, prefix);
WritePositionTaggedLiteral(writer, suffix);
}
else
{
for (var i = 0; i < values.Length; i++)
{
var attrVal = values[i];
var val = attrVal.Value;
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
if (val.Value == null)
{
// Nothing to write
continue;
}
// The special cases here are that the value we're writing might already be a string, or that the
// value might be a bool. If the value is the bool 'true' we want to write the attribute name
// instead of the string 'true'. If the value is the bool 'false' we don't want to write anything.
// Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself.
string stringValue;
// Intentionally using is+cast here for performance reasons. This is more performant than as+bool?
// because of boxing.
if (val.Value is bool)
{
if ((bool)val.Value)
{
stringValue = name;
}
else
{
continue;
}
}
else
{
stringValue = val.Value as string;
}
if (first)
{
WritePositionTaggedLiteral(writer, prefix);
first = false;
}
else
{
WritePositionTaggedLiteral(writer, attrVal.Prefix);
}
// Calculate length of the source span by the position of the next value (or suffix)
var sourceLength = next.Position - attrVal.Value.Position;
BeginContext(attrVal.Value.Position, sourceLength, isLiteral: attrVal.Literal);
// The extra branching here is to ensure that we call the Write*To(string) overload whe
// possible.
if (attrVal.Literal && stringValue != null)
{
WriteLiteralTo(writer, stringValue);
}
else if (attrVal.Literal)
{
WriteLiteralTo(writer, val.Value);
}
else if (stringValue != null)
{
WriteTo(writer, stringValue);
}
else
{
WriteTo(writer, val.Value);
}
EndContext();
wroteSomething = true;
}
if (wroteSomething)
{
WritePositionTaggedLiteral(writer, suffix);
}
}
}
public virtual string Href([NotNull] string contentPath)
{
if (_urlHelper == null)
{
_urlHelper = Context.RequestServices.GetRequiredService();
}
return _urlHelper.Content(contentPath);
}
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 (RenderBodyDelegate == null)
{
var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody));
throw new InvalidOperationException(message);
}
_renderedBody = true;
return new HelperResult(RenderBodyDelegate);
}
///
/// 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 (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 TaskHelper.WaitAndThrowIfFaulted(task);
}
///
/// 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 async Task RenderSectionAsync([NotNull] string name, bool required)
{
EnsureMethodCanBeInvoked(nameof(RenderSectionAsync));
return await RenderSectionAsyncCore(name, required);
}
private async Task RenderSectionAsyncCore(string sectionName, bool required)
{
if (_renderedSections.Contains(sectionName))
{
var message = Resources.FormatSectionAlreadyRendered(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));
}
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 FulshAsync is
/// called. For example, call to send anti-forgery 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.RazorPage_YouCannotFlushWhileInAWritingScope);
}
// 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(nameof(FlushAsync));
throw new InvalidOperationException(message);
}
await Output.FlushAsync();
return HtmlString.Empty;
}
///
public void EnsureBodyAndSectionsWereRendered()
{
// If PreviousSectionWriters is set, ensure all defined sections were rendered.
if (PreviousSectionWriters != null)
{
var sectionsNotRendered = PreviousSectionWriters.Keys.Except(_renderedSections,
StringComparer.OrdinalIgnoreCase);
if (sectionsNotRendered.Any())
{
var sectionNames = string.Join(", ", sectionsNotRendered);
throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames));
}
}
// If BodyContent is set, ensure it was rendered.
if (RenderBodyDelegate != null && !_renderedBody)
{
// If a body was defined, then RenderBody should have been called.
var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody));
throw new InvalidOperationException(message);
}
}
public void BeginContext(int position, int length, bool isLiteral)
{
PageExecutionContext?.BeginContext(position, length, isLiteral);
}
public void EndContext()
{
PageExecutionContext?.EndContext();
}
///
/// Sets anti-forgery cookie and X-Frame-Options header on the response.
///
/// A that returns a .
/// Call this method to send anti-forgery 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 void EnsureMethodCanBeInvoked(string methodName)
{
if (PreviousSectionWriters == null)
{
throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName));
}
}
}
}