Skip to content

Commit

Permalink
Fix global resource registration in master pages
Browse files Browse the repository at this point in the history
When the RequiredResource controls are
added at the end of the page, the
DefaultDotvvmViewBuilder would crash.
This specialcases the RequiredResource,
since it doesn't render anything, it's safe to include anywhere
  • Loading branch information
exyi committed Apr 27, 2024
1 parent 091821f commit dfce271
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 10 deletions.
39 changes: 31 additions & 8 deletions src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using DotVVM.Framework.Binding;
Expand Down Expand Up @@ -107,7 +108,7 @@ private void PerformMasterPageComposition(DotvvmView childPage, DotvvmView maste
var placeHolders = GetMasterPageContentPlaceHolders(masterPage);

// find contents
var contents = GetChildPageContents(childPage, placeHolders);
var (contents, auxControls) = GetChildPageContents(childPage, placeHolders);

// perform the composition
foreach (var content in contents)
Expand All @@ -133,7 +134,10 @@ private void PerformMasterPageComposition(DotvvmView childPage, DotvvmView maste
content.SetValue(Internal.MarkupFileNameProperty, childPage.GetValue(Internal.MarkupFileNameProperty));
content.SetValue(Internal.ReferencedViewModuleInfoProperty, childPage.GetValue(Internal.ReferencedViewModuleInfoProperty));
}


foreach (var control in auxControls)
masterPage.Children.Add(control);

// copy the directives from content page to the master page (except the @masterpage)
masterPage.ViewModelType = childPage.ViewModelType;
}
Expand All @@ -160,11 +164,30 @@ private List<ContentPlaceHolder> GetMasterPageContentPlaceHolders(DotvvmControl
/// <summary>
/// Checks that the content page does not contain invalid content.
/// </summary>
private List<Content> GetChildPageContents(DotvvmView childPage, List<ContentPlaceHolder> parentPlaceHolders)
private (List<Content> contents, List<DotvvmControl> auxiliaryControls) GetChildPageContents(DotvvmView childPage, List<ContentPlaceHolder> parentPlaceHolders)
{
// make sure that the body contains only whitespace and Content controls
var nonContentElements =
childPage.Children.Where(c => !((c is RawLiteral && ((RawLiteral)c).IsWhitespace) || (c is Content)));
// make sure that the body contains only Content controls (and whitespace and auxiliary controls)
var nonContentElements = new List<DotvvmControl>();
// controls which don't render anything and may be placed anywhere (RequireResource)
var auxiliaryControls = new List<DotvvmControl>();
foreach (var child in childPage.Children)
{
if (child is RawLiteral { IsWhitespace: true })
{
// ignored
}
else if (child is RequiredResource)
{
child.Parent = null; // childPage view is discarded
auxiliaryControls.Add(child);
}
else if (child is Content)
{
// handled bellow
}
else
nonContentElements.Add(child);
}
if (nonContentElements.Any())
{
// show all error lines
Expand All @@ -182,10 +205,10 @@ private List<Content> GetChildPageContents(DotvvmView childPage, List<ContentPla
.ToList();
if (contents.FirstOrDefault(c => c.Parent != childPage) is {} invalidContent)
{
throw new DotvvmControlException(invalidContent, "The control <dot:Content /> cannot be placed inside any control!");
throw new DotvvmControlException(invalidContent, $"The control <dot:Content ContentPlaceHolderID='{invalidContent.ContentPlaceHolderID ?? "<null>"}' /> cannot be placed inside any control!");
}

return contents;
return (contents, auxiliaryControls);
}
}
}
4 changes: 2 additions & 2 deletions src/Framework/Testing/ControlTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ public ControlTestHelper(bool debug = true, Action<DotvvmConfiguration>? config
ClaimsPrincipal? user = null,
CultureInfo? culture = null)
{
if (!markup.Contains("<body"))
if (!markup.Contains("<body") && !markup.Contains("<dot:Content"))
{
markup = $"<body Validation.Enabled=false >\n{markup}\n{(renderResources ? "" : "<tc:FakeBodyResourceLink />")}\n</body>";
}
else if (!renderResources)
{
markup = "<tc:FakeBodyResourceLink />" + markup;
}
if (!markup.Contains("<head"))
if (!markup.Contains("<head") && !markup.Contains("<dot:Content"))
{
markup = $"<head></head>\n{markup}";
}
Expand Down
61 changes: 61 additions & 0 deletions src/Tests/ControlTests/ServerSideStyleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,67 @@ @viewModel int
check.CheckString(r.FormattedHtml, fileExtension: "html");
}



[TestMethod]
public async Task AddResourceWithMasterPage()
{
var cth = createHelper(c => {
c.Resources.Register("test-resource1", new InlineScriptResource("alert(1)"));
c.Resources.Register("test-resource2", new InlineScriptResource("alert(2)"));
c.Resources.Register("test-resource3", new InlineScriptResource("alert(3)"));
c.Resources.Register("test-resource4", new InlineScriptResource("alert(4)"));
c.Resources.Register("test-resource5", new InlineScriptResource("alert(5)"));
c.Styles.RegisterRoot()
.AddRequiredResource(cx => {
return cx.Control.TreeRoot.Directives.GetValueOrDefault("custom_resource_import", new()).Select(d => d.Value).ToArray();

Check failure on line 461 in src/Tests/ControlTests/ServerSideStyleTests.cs

View workflow job for this annotation

GitHub Actions / .NET unit tests (windows-2022)

'Dictionary<string, List<IAbstractDirective>>' does not contain a definition for 'GetValueOrDefault' and no accessible extension method 'GetValueOrDefault' accepting a first argument of type 'Dictionary<string, List<IAbstractDirective>>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 461 in src/Tests/ControlTests/ServerSideStyleTests.cs

View workflow job for this annotation

GitHub Actions / .NET unit tests (windows-2022)

'Dictionary<string, List<IAbstractDirective>>' does not contain a definition for 'GetValueOrDefault' and no accessible extension method 'GetValueOrDefault' accepting a first argument of type 'Dictionary<string, List<IAbstractDirective>>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 461 in src/Tests/ControlTests/ServerSideStyleTests.cs

View workflow job for this annotation

GitHub Actions / Build all projects without errors

'Dictionary<string, List<IAbstractDirective>>' does not contain a definition for 'GetValueOrDefault' and no accessible extension method 'GetValueOrDefault' accepting a first argument of type 'Dictionary<string, List<IAbstractDirective>>' could be found (are you missing a using directive or an assembly reference?)
});
});

var r = await cth.RunPage(typeof(object), """

<dot:Content ContentPlaceHolderID="nested-body">
real body
</dot:Content>

<dot:RequiredResource Name="test-resource5" />
""",
directives: """
@masterPage master1.dotmaster
@custom_resource_import test-resource1
""",
markupFiles: new Dictionary<string, string> {
["master1.dotmaster"] = """
@viewModel object
@masterPage master2.dotmaster
@custom_resource_import test-resource2

<dot:Content ContentPlaceHolderID="head">
<dot:RequiredResource Name=test-resource4 />
</dot:Content>
<dot:Content ContentPlaceHolderID="body">
<div class="master1">
<dot:ContentPlaceHolder ID="nested-body" />
</div>
</dot:Content>
""",
["master2.dotmaster"] = """
@custom_resource_import test-resource3
@viewModel object

<head>
<dot:ContentPlaceHolder ID="head" />
</head>
<body>
<dot:ContentPlaceHolder ID="body" />
</body>
"""
}, renderResources: true);
check.CheckString(r.OutputString, fileExtension: "html");
}

public class BasicTestViewModel: DotvvmViewModelBase
{
[Bind(Name = "int")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

<head>




<!-- Resource knockout of type ScriptResource. Pointing to EmbeddedResourceLocation. -->
<script src=/dotvvmResource/knockout/knockout defer></script>
<!-- Resource dotvvm.internal of type ScriptModuleResource. Pointing to EmbeddedResourceLocation. -->
<script src=/dotvvmResource/dotvvm--internal/dotvvm--internal type=module defer></script>
<!-- Resource dotvvm of type InlineScriptResource. -->

<!-- Resource dotvvm.debug of type ScriptResource. Pointing to EmbeddedResourceLocation. -->
<script src=/dotvvmResource/dotvvm--debug/dotvvm--debug defer></script>
</head>
<body>

<div class=master1>

real body

</div>


<!-- Resource test-resource4 of type InlineScriptResource. -->
<script>alert(4)</script>
<!-- Resource test-resource3 of type InlineScriptResource. -->
<script>alert(3)</script>
<!-- Resource test-resource2 of type InlineScriptResource. -->
<script>alert(2)</script>
<!-- Resource test-resource5 of type InlineScriptResource. -->
<script>alert(5)</script>
<!-- Resource test-resource1 of type InlineScriptResource. -->
<script>alert(1)</script>
<input type=hidden id=__dot_viewmodel_root value='{
"viewModel": {
"$csrfToken": "Not a CSRF token."
},
"url": "/testpage",
"virtualDirectory": "",
"renderedResources": [
"knockout",
"dotvvm.internal",
"dotvvm",
"dotvvm.debug",
"test-resource4",
"test-resource3",
"test-resource2",
"test-resource5",
"test-resource1"
],
"typeMetadata": {}
}' /><script defer src="data:text/javascript;base64,ZG90dnZtLm9wdGlvbnMuY29tcHJlc3NQT1NUPWZhbHNlOwp3aW5kb3cuZG90dnZtLmluaXQoImVuLVVTIik7"></script></body>

0 comments on commit dfce271

Please sign in to comment.