diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index a76d917405..f6dfb487a3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -446,6 +446,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"), p0, p1, p2, p3, p4, p5); } + /// + /// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered. + /// + internal static string LayoutHasCircularReference + { + get { return GetString("LayoutHasCircularReference"); } + } + + /// + /// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered. + /// + 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); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 574f1026b4..c2a758371b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -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 /// The HTML encoder. /// Determines if the view is to be executed as a partial. /// pages - 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; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index bd3dede0e4..4431f51965 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -200,4 +200,7 @@ @{3} "{4}, {5}" + + A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered. + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 915ab50456..3606c509bb 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -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(); viewEngine.Setup(p => p.FindPage(It.IsAny(), "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(); viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/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(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout")) + .Returns(new RazorPageResult("_Layout", layout)); + + var view = new RazorView(viewEngine.Object, + Mock.Of(), + CreateViewStartProvider(), + page, + new CommonTestEncoder(), + isPartial: false); + var viewContext = CreateViewContext(view); + + // Act and Assert + var exception = await Assert.ThrowsAsync(() => 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(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout")) + .Returns(new RazorPageResult("_Layout", layout1)); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout2")) + .Returns(new RazorPageResult("_Layout2", layout2)); + + var view = new RazorView(viewEngine.Object, + Mock.Of(), + CreateViewStartProvider(), + page, + new CommonTestEncoder(), + isPartial: false); + var viewContext = CreateViewContext(view); + + // Act and Assert + var exception = await Assert.ThrowsAsync(() => 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(); viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml")) .Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));