diff --git a/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/Filters/ResourceUrlFilter.cs b/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/Filters/ResourceUrlFilter.cs index d913462d72c..a1982786d37 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/Filters/ResourceUrlFilter.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/Filters/ResourceUrlFilter.cs @@ -35,10 +35,13 @@ public ValueTask 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. - if (!_options.DebugMode - && !String.IsNullOrEmpty(_options.CdnBaseUrl) - && !Uri.TryCreate(resourcePath, UriKind.Absolute, out var uri)) + // 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) && + // 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.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { resourcePath = _options.CdnBaseUrl + resourcePath; } diff --git a/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceDefinition.cs b/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceDefinition.cs index a2c5f01712b..58fdf60f466 100644 --- a/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceDefinition.cs +++ b/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceDefinition.cs @@ -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; } @@ -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 - && !String.IsNullOrEmpty(settings.CdnBaseUrl) - && !Uri.TryCreate(url, UriKind.Absolute, out var uri)) + // 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) && + // 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.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { url = settings.CdnBaseUrl + url; } diff --git a/src/OrchardCore/OrchardCore.ResourceManagement/Razor/ResourceCdnHelperExtensions.cs b/src/OrchardCore/OrchardCore.ResourceManagement/Razor/ResourceCdnHelperExtensions.cs index 952422ea98e..6888c94ecff 100644 --- a/src/OrchardCore/OrchardCore.ResourceManagement/Razor/ResourceCdnHelperExtensions.cs +++ b/src/OrchardCore/OrchardCore.ResourceManagement/Razor/ResourceCdnHelperExtensions.cs @@ -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. - if (!options.DebugMode - && !String.IsNullOrEmpty(options.CdnBaseUrl) - && !Uri.TryCreate(resourcePath, UriKind.Absolute, out var uri)) + // 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) && + // 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.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { resourcePath = options.CdnBaseUrl + resourcePath; } diff --git a/test/OrchardCore.Tests/ResourceManagement/ResourceDefinitionTests.cs b/test/OrchardCore.Tests/ResourceManagement/ResourceDefinitionTests.cs index d24ec40c4b7..2c37f15b5f8 100644 --- a/test/OrchardCore.Tests/ResourceManagement/ResourceDefinitionTests.cs +++ b/test/OrchardCore.Tests/ResourceManagement/ResourceDefinitionTests.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; @@ -10,7 +11,6 @@ namespace OrchardCore.Tests.ResourceManagement { public class ResourceDefinitionTests { - private const string basePath = "http://host"; private readonly ResourceManifest _resourceManifest; public ResourceDefinitionTests() @@ -18,116 +18,179 @@ 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("")] + [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"]); @@ -135,15 +198,41 @@ public void GetStyleResourceWithCdnUrl() 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"]); @@ -151,8 +240,11 @@ public void GetStyleResourceWithDebugCdnUrl() 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") @@ -160,7 +252,7 @@ public void GetStyleResourceWithAttributes() .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"]); @@ -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"]);