Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix global resource registration in master pages #1811

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 31 additions & 8 deletions src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs
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
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
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.TryGetValue("custom_resource_import", out var x) ? x.Select(d => d.Value).ToArray() : new string[0];
});
});

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
@@ -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>