Throw an exception if layouts have circular references.

Fixes #3100
This commit is contained in:
Pranav K 2015-09-11 14:53:58 -07:00
parent c79469c3b3
commit 887ab64d75
4 changed files with 124 additions and 3 deletions

View File

@ -446,6 +446,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"), p0, p1, p2, p3, p4, p5);
}
/// <summary>
/// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.
/// </summary>
internal static string LayoutHasCircularReference
{
get { return GetString("LayoutHasCircularReference"); }
}
/// <summary>
/// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.
/// </summary>
internal static string FormatLayoutHasCircularReference(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("LayoutHasCircularReference"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Mvc.Rendering;
@ -35,7 +36,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <param name="htmlEncoder">The HTML encoder.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
/// pages</param>
public RazorView(IRazorViewEngine viewEngine,
public RazorView(
IRazorViewEngine viewEngine,
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider,
IRazorPage razorPage,
@ -170,7 +172,8 @@ namespace Microsoft.AspNet.Mvc.Razor
RazorPage.Layout = layout;
}
private async Task RenderLayoutAsync(ViewContext context,
private async Task RenderLayoutAsync(
ViewContext context,
IBufferedTextWriter bodyWriter)
{
// A layout page can specify another layout page. We'll need to continue
@ -192,6 +195,15 @@ namespace Microsoft.AspNet.Mvc.Razor
var layoutPage = GetLayoutPage(context, previousPage.Layout);
if (renderedLayouts.Count > 0 &&
renderedLayouts.Any(l => string.Equals(l.Path, layoutPage.Path, StringComparison.Ordinal)))
{
// If the layout has been previously rendered as part of this view, we're potentially in a layout
// rendering cycle.
throw new InvalidOperationException(
Resources.FormatLayoutHasCircularReference(previousPage.Path, layoutPage.Path));
}
// Notify the previous page that any writes that are performed on it are part of sections being written
// in the layout.
previousPage.IsLayoutBeingRendered = true;

View File

@ -200,4 +200,7 @@
@{3} "{4}, {5}"</value>
</data>
<data name="LayoutHasCircularReference" xml:space="preserve">
<value>A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.</value>
</data>
</root>

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Actions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Framework.WebEncoders.Testing;
@ -568,11 +567,13 @@ namespace Microsoft.AspNet.Mvc.Razor
await v.RenderSectionAsync("sectionA");
});
});
nestedLayout.Path = "NestedLayout";
var baseLayout = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
v.RenderSection("sectionB");
});
baseLayout.Path = "Layout";
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "NestedLayout"))
@ -784,6 +785,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml";
});
layout1.Path = "~/Shared/Layout1.cshtml";
var layout2 = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
@ -791,6 +794,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.Write(v.RenderSection("bar"));
v.RenderBodyPublic();
});
layout2.Path = "~/Shared/Layout2.cshtml";
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", layout1));
@ -812,6 +817,87 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(expected, viewContext.Writer.ToString());
}
[Fact]
public async Task RenderAsync_Throws_IfLayoutPageReferencesSelf()
{
// Arrange
var expectedMessage = "A circular layout reference was detected when rendering " +
"'Shared/Layout.cshtml'. The layout page 'Shared/Layout.cshtml' has already been rendered.";
var page = new TestableRazorPage(v =>
{
v.Layout = "_Layout";
});
var layout = new TestableRazorPage(v =>
{
v.Layout = "_Layout";
v.RenderBodyPublic();
});
layout.Path = "Shared/Layout.cshtml";
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "_Layout"))
.Returns(new RazorPageResult("_Layout", layout));
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
new CommonTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
// Assert
Assert.Equal(expectedMessage, exception.Message);
}
[Fact]
public async Task RenderAsync_Throws_IfNestedLayoutPagesResultInCyclicReferences()
{
// Arrange
var expectedMessage = "A circular layout reference was detected when rendering " +
"'/Shared/Layout2.cshtml'. The layout page 'Shared/_Layout.cshtml' has already been rendered.";
var page = new TestableRazorPage(v =>
{
v.Layout = "_Layout";
});
var layout1 = new TestableRazorPage(v =>
{
v.Layout = "_Layout2";
v.RenderBodyPublic();
});
layout1.Path = "Shared/_Layout.cshtml";
var layout2 = new TestableRazorPage(v =>
{
v.Layout = "_Layout";
v.RenderBodyPublic();
});
layout2.Path = "/Shared/Layout2.cshtml";
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "_Layout"))
.Returns(new RazorPageResult("_Layout", layout1));
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "_Layout2"))
.Returns(new RazorPageResult("_Layout2", layout2));
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
new CommonTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
// Assert
Assert.Equal(expectedMessage, exception.Message);
}
[Fact]
public async Task RenderAsync_ExecutesNestedLayoutsWithNestedSections()
{
@ -848,6 +934,8 @@ namespace Microsoft.AspNet.Mvc.Razor
await writer.WriteLineAsync(htmlEncoder.HtmlEncode(v.RenderSection("foo").ToString()));
});
});
nestedLayout.Path = "~/Shared/Layout2.cshtml";
var baseLayout = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
@ -855,6 +943,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic();
v.Write(v.RenderSection("foo"));
});
baseLayout.Path = "~/Shared/Layout1.cshtml";
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));