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

Correctly generate cdn base url for local scripts #8042

Merged
merged 6 commits into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments argu
resourcePath = _httpContextAccessor.HttpContext.Request.PathBase.Add(resourcePath.Substring(1)).Value;
}

// Don't prefix cdn if the path is absolute, or is in debug mode.
// Don't prefix cdn if the path includes a protocol, i.e. is an external url, or is in debug mode.
if (!_options.DebugMode
&& !String.IsNullOrEmpty(_options.CdnBaseUrl)
&& !Uri.TryCreate(resourcePath, UriKind.Absolute, out var uri))
// Don't evaluate with Uri.TryCreate as it produces incorrect results on Linux.
&& !resourcePath.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
deanmarcussen marked this conversation as resolved.
Show resolved Hide resolved
&& !resourcePath.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !resourcePath.StartsWith("//", StringComparison.OrdinalIgnoreCase))
deanmarcussen marked this conversation as resolved.
Show resolved Hide resolved
deanmarcussen marked this conversation as resolved.
Show resolved Hide resolved
{
resourcePath = _options.CdnBaseUrl + resourcePath;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ public ResourceDefinition SetAttribute(string name, string value)
return this;
}

public ResourceDefinition SetBasePath(string virtualPath)
public ResourceDefinition SetBasePath(string basePath)
{
_basePath = virtualPath;
_basePath = basePath;
return this;
}

Expand Down Expand Up @@ -228,10 +228,13 @@ public TagBuilder GetTagBuilder(RequireSettings settings,
url = fileVersionProvider.AddFileVersionToPath(applicationPath, url);
}

// Don't prefix cdn if the path is absolute, or is in debug mode.
if (!settings.DebugMode
// Don't prefix cdn if the path includes a protocol, i.e. is an external url, or is in debug mode.
if (url != null && !settings.DebugMode
&& !String.IsNullOrEmpty(settings.CdnBaseUrl)
&& !Uri.TryCreate(url, UriKind.Absolute, out var uri))
// Don't evaluate with Uri.TryCreate as it produces incorrect results on Linux.
&& !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !url.StartsWith("//", StringComparison.OrdinalIgnoreCase))
{
url = settings.CdnBaseUrl + url;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ public static string ResourceUrl(this IOrchardHelper orchardHelper, string resou
resourcePath = fileVersionProvider.AddFileVersionToPath(orchardHelper.HttpContext.Request.PathBase, resourcePath);
}

// Don't prefix cdn if the path is absolute, or is in debug mode.
// Don't prefix cdn if the path includes a protocol, i.e. is an external url, or is in debug mode.
if (!options.DebugMode
&& !String.IsNullOrEmpty(options.CdnBaseUrl)
&& !Uri.TryCreate(resourcePath, UriKind.Absolute, out var uri))
// Don't evaluate with Uri.TryCreate as it produces incorrect results on Linux.
&& !resourcePath.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !resourcePath.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !resourcePath.StartsWith("//", StringComparison.OrdinalIgnoreCase))
{
resourcePath = options.CdnBaseUrl + resourcePath;
}
Expand Down
176 changes: 136 additions & 40 deletions test/OrchardCore.Tests/ResourceManagement/ResourceDefinitionTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
Expand All @@ -10,157 +11,248 @@ namespace OrchardCore.Tests.ResourceManagement
{
public class ResourceDefinitionTests
{
private const string basePath = "http://host";
private readonly ResourceManifest _resourceManifest;

public ResourceDefinitionTests()
{
_resourceManifest = new ResourceManifest();
}

[Fact]
public void GetScriptResourceWithUrl()
// The application path is empty if it is the host site
// or contains the base path of a tenant.
[Theory]
[InlineData("")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtkech i added the correct values for the application path here (base path)
but while I am in this piece of code what is the correct value for a virtual path?
and I will add it as well.

leasing slash? Or without?

Copy link
Member

@jtkech jtkech Dec 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/virtual/tenant

When mapping to a virtual path

PathBase = /virtual
Path = /tenant/...

Then in our middleware we set

PathBase = /virtual/tenant
Path = /...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtkech I updated this to add a test for the WithBasePath / SetBasePath

I think that feature, despite having some notation of being a virtualPath is disconnected from what we would actually call a virtualPath (i.e. under IIS).

Not sure how useful the WithBasePath actually is, but it's there, and works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deanmarcussen

That's okay it's just a question of semantics, as you want ;)

About the base path i was thinking about the httpRequest PathBase that may contain a virtual path (i.e. under IIS) and or a tenant prefix, the tenant prefix being a kind of virtual path, but at the end the whole being a base path.

So forget the virtual term, just use basePath, and yes better to have

ResourceDefinition SetBasePath(string basePath)

That said in the tests, for me /tenant and /virtual/tenant are not applicationPath, but basePath, and then in /tenant/scripts/script.js, /scripts/script.js is a path


Okay just saw in the ResourceDefinition code the usage of GetTagBuilder(..., string applicationPath, ...) which is called somewhere by GetHtmlContent(string appPath) which is called by GetHtmlContent(_options.ContentBasePath), and where _options.ContentBasePath is initialized with the httpRequest PathBase ;)

        options.ContentBasePath = _httpContextAccessor.HttpContext.Request.PathBase.Value;

So okay, we could say that applicationPath is the same as basePath, but here i prefer basePath, at least tenantPath as an application can have multiple tenants, but each one being a kind of an isolated app ;)


https://base.com is not a basePath, nor an applicationPath, but a full Uri (Url is okay), and e.g. in the full uri https://base.com/virtual/tenant/scripts/script.js, /virtual/tenant is a basePath and /scripts/script.js is a path

Maybe good to have a test with /virtual/tenant, just to do a test with a basePath having 2 segments

But as said it's just a question of terminology, so as you want ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree @jtkech .

The terminology inside the resource definition is not quite what we are used to. So it was confusing even figuring out what they should be.

I've added /virtual/tenant for the two segments (makes sense)

And remove the full uri https://base.com. While it would work, I'm just not sure it makes sense.

Equally (and I think this is probably a different issue, but noone has ever raised it so I am ignoring it.)

I'm not sure how a base path would work in combination with a tenant prefix.

But I'm not sure exactly what you would use the basePath for anyway.

So I just test that it's behaviour is the same as it is currently so if it is being used by anyone we know there is no regression.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think doing file:// is a good idea. That would suggest that using the resource manager to load files was a good idea. Which it's not. Wouldn't work anyway the browser would block it - I would hope ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deanmarcussen No problem, that's okay ;)

Just for info, using file:///C:/Sources/OrchardCore/src/OrchardCore.Cms.Web/App_Data/Sites/Default/Media/about-bg.jpg works on my machine

File Url

Copy link
Member Author

@deanmarcussen deanmarcussen Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but try loading a script that way, on an actual site ;)
How would I get about-bg to my computer. and in C: when I don't even have a C:

I assume you are having a laugh btw ;)

It's a hot path, so I don't want to add more linq to support something that shouldn't be supported.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't work anyway the browser would block it - I would hope ;)

Yes, it was just to show that the browser doesn't block an unc path starting with file://
It is on my local machine but i thought about something like file://someserver/somepath

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I give in ;)

Added file://

[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetScriptResourceWithUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl("~/foo.js", "~/foo.debug.js")
.SetCdn("https://cdn.tld/foo.js", "https://cdn.tld/foo.debug.js");

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal($"{applicationPath}/foo.js", tagBuilder.Attributes["src"]);
}

[Theory]
[InlineData("/base")]
public void GetScriptResourceWithBasePath(string basePath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl("~/foo.js", "~/foo.debug.js")
.SetBasePath(basePath);

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, String.Empty, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal($"{basePath}/foo.js", tagBuilder.Attributes["src"]);
}

[Fact]
public void GetScriptResourceWithDebugUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetScriptResourceWithDebugUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl("~/foo.js", "~/foo.debug.js")
.SetCdn("https://cdn.tld/foo.js", "https://cdn.tld/foo.debug.js");

var requireSettings = new RequireSettings { DebugMode = true, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal($"{basePath}/foo.debug.js", tagBuilder.Attributes["src"]);
Assert.Equal($"{applicationPath}/foo.debug.js", tagBuilder.Attributes["src"]);
}

[Fact]
public void GetScriptResourceWithCdnUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetScriptResourceWithCdnUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl("~/foo.js", "~/foo.debug.js")
.SetCdn("https://cdn.tld/foo.js", "https://cdn.tld/foo.debug.js");

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = true };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal("https://cdn.tld/foo.js", tagBuilder.Attributes["src"]);
}

[Fact]
public void GetScriptResourceWithDebugCdnUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetScriptResourceWithDebugCdnUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl("~/foo.js", "~/foo.debug.js")
.SetCdn("https://cdn.tld/foo.js", "https://cdn.tld/foo.debug.js");

var requireSettings = new RequireSettings { DebugMode = true, CdnMode = true };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var requireSettings = new RequireSettings { DebugMode = true, CdnMode = true, CdnBaseUrl = "https://hostcdn.net" };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal("https://cdn.tld/foo.debug.js", tagBuilder.Attributes["src"]);
}

[Fact]
public void GetScriptResourceWithInlineContent()
[Theory]
[InlineData("", "~/foo.js", "https://hostcdn.net/foo.js")]
[InlineData("/tenant", "~/foo.js", "https://hostcdn.net/tenant/foo.js")]
[InlineData("", "//external.com/foo.js", "//external.com/foo.js")]
[InlineData("/tenant", "//external.com/foo.js", "//external.com/foo.js")]
[InlineData("", "http://external.com/foo.js", "http://external.com/foo.js")]
[InlineData("/tenant", "http://external.com/foo.js", "http://external.com/foo.js")]
[InlineData("", "https://external.com/foo.js", "https://external.com/foo.js")]
[InlineData("/tenant", "https://external.com/foo.js", "https://external.com/foo.js")]
public void GetLocalScriptResourceWithCdnBaseUrl(string applicationPath, string url, string expected)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetUrl(url, url);

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = true, CdnBaseUrl = "https://hostcdn.net" };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal(expected, tagBuilder.Attributes["src"]);
}

[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetScriptResourceWithInlineContent(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineScript("foo")
.SetInnerContent("console.log('foo');");

var requireSettings = new RequireSettings();
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var requireSettings = new RequireSettings()
.UseCdnBaseUrl("https://hostcdn.net");
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("script", tagBuilder.TagName);
Assert.Equal("console.log('foo');", ReadIHtmlContent(tagBuilder.InnerHtml));
}

[Fact]
public void GetStyleResourceWithUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl("~/foo.css", "~/foo.debug.css")
.SetCdn("https://cdn.tld/foo.css", "https://cdn.tld/foo.debug.css");

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Assert.Equal("stylesheet", tagBuilder.Attributes["rel"]);
Assert.Equal($"{basePath}/foo.css", tagBuilder.Attributes["href"]);
Assert.Equal($"{applicationPath}/foo.css", tagBuilder.Attributes["href"]);
}

[Fact]
public void GetStyleResourceWithDebugUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithDebugUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl("~/foo.css", "~/foo.debug.css")
.SetCdn("https://cdn.tld/foo.css", "https://cdn.tld/foo.debug.css");

var requireSettings = new RequireSettings { DebugMode = true, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Assert.Equal("stylesheet", tagBuilder.Attributes["rel"]);
Assert.Equal($"{basePath}/foo.debug.css", tagBuilder.Attributes["href"]);
Assert.Equal($"{applicationPath}/foo.debug.css", tagBuilder.Attributes["href"]);
}

[Fact]
public void GetStyleResourceWithCdnUrl()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithCdnUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl("~/foo.css", "~/foo.debug.css")
.SetCdn("https://cdn.tld/foo.css", "https://cdn.tld/foo.debug.css");

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = true };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Assert.Equal("stylesheet", tagBuilder.Attributes["rel"]);
Assert.Equal("https://cdn.tld/foo.css", tagBuilder.Attributes["href"]);
}

[Fact]
public void GetStyleResourceWithDebugCdnUrl()
[Theory]
[InlineData("", "~/foo.css", "https://hostcdn.net/foo.css")]
[InlineData("/tenant", "~/foo.css", "https://hostcdn.net/tenant/foo.css")]
[InlineData("", "//external.com/foo.css", "//external.com/foo.css")]
[InlineData("/tenant", "//external.com/foo.css", "//external.com/foo.css")]
[InlineData("", "http://external.com/foo.css", "http://external.com/foo.css")]
[InlineData("/tenant", "http://external.com/foo.css", "http://external.com/foo.css")]
[InlineData("", "https://external.com/foo.css", "https://external.com/foo.css")]
[InlineData("/tenant", "https://external.com/foo.css", "https://external.com/foo.css")]
public void GetLocalStyleResourceWithCdnBaseUrl(string applicationPath, string url, string expected)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl(url, url);

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = true, CdnBaseUrl = "https://hostcdn.net" };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Assert.Equal("stylesheet", tagBuilder.Attributes["rel"]);
Assert.Equal(expected, tagBuilder.Attributes["href"]);
}

[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithDebugCdnUrl(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl("~/foo.css", "~/foo.debug.css")
.SetCdn("https://cdn.tld/foo.css", "https://cdn.tld/foo.debug.css");

var requireSettings = new RequireSettings { DebugMode = true, CdnMode = true };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Assert.Equal("stylesheet", tagBuilder.Attributes["rel"]);
Assert.Equal("https://cdn.tld/foo.debug.css", tagBuilder.Attributes["href"]);
}

[Fact]
public void GetStyleResourceWithAttributes()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithAttributes(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetUrl("~/foo.css", "~/foo.debug.css")
.SetAttribute("id", "foo")
.SetAttribute("media", "all");

var requireSettings = new RequireSettings { DebugMode = false, CdnMode = false };
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("link", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Expand All @@ -169,14 +261,18 @@ public void GetStyleResourceWithAttributes()
Assert.Equal("all", tagBuilder.Attributes["media"]);
}

[Fact]
public void GetStyleResourceWithInlineContent()
[Theory]
[InlineData("")]
[InlineData("/tenant")]
[InlineData("/virtualpath/tenant")]
public void GetStyleResourceWithInlineContent(string applicationPath)
{
var resourceDefinition = _resourceManifest.DefineStyle("foo")
.SetInnerContent("body { background-color: white; }");

var requireSettings = new RequireSettings();
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, basePath, StubFileVersionProvider.Instance);
var requireSettings = new RequireSettings()
.UseCdnBaseUrl("https://cdn.net");
var tagBuilder = resourceDefinition.GetTagBuilder(requireSettings, applicationPath, StubFileVersionProvider.Instance);

Assert.Equal("style", tagBuilder.TagName);
Assert.Equal("text/css", tagBuilder.Attributes["type"]);
Expand Down