diff --git a/docs/en/framework/ui/blazor/branding.md b/docs/en/framework/ui/blazor/branding.md
index d6e7e4d358..a30ec40b78 100644
--- a/docs/en/framework/ui/blazor/branding.md
+++ b/docs/en/framework/ui/blazor/branding.md
@@ -41,4 +41,12 @@ The result will be like shown below:
* `LogoUrl`: A URL to show the application logo.
* `LogoReverseUrl`: A URL to show the application logo on a reverse color theme (dark, for example).
+ABP's built-in Blazor themes resolve these URLs for the current application. `logo.png`, `/logo.png` and `~/logo.png` all mean the same application relative URL and include the base path of the application, so they keep working when it is deployed to a non-root path, like an IIS virtual directory. A URL that already contains the path gets it twice. External URLs are used as they are. `LogoReverseUrl` is used by the themes that have a dark style, like LeptonX, and falls back to `LogoUrl`.
+
+> **Note**: The `` of the host page has to match the path the application is served from, like ``.
+
+A logo that the project defines in its own CSS is not resolved. The LeptonX Lite startup templates set the logo that way, so remove that declaration to deploy them to a non-root path.
+
+To resolve a branding URL in a custom theme or component, use `NavigationManager.ResolveBrandingUrl(...)` with `@using Volo.Abp.AspNetCore.Components.Web.Theming.Branding`, or `ResolveBrandingCssUrl(...)` when it is rendered into `url('...')` in CSS.
+
> **Tip**: `IBrandingProvider` is used in every page refresh. For a multi-tenant application, you can return a tenant specific application name to customize it per tenant.
diff --git a/docs/en/framework/ui/mvc-razor-pages/branding.md b/docs/en/framework/ui/mvc-razor-pages/branding.md
index 9c9f5c6a37..7543b82f74 100644
--- a/docs/en/framework/ui/mvc-razor-pages/branding.md
+++ b/docs/en/framework/ui/mvc-razor-pages/branding.md
@@ -43,9 +43,11 @@ The result will be like shown below:
* `LogoUrl`: A URL to show the application logo.
* `LogoReverseUrl`: A URL to show the application logo on a reverse color theme (dark, for example).
-ABP's built-in MVC themes resolve the branding URLs for the current request. `/logo.png`, `logo.png` and `~/logo.png` are treated as application relative URLs and include the `PathBase` of the request, so they keep working when the application is deployed to a non-root path, like an IIS virtual directory. Absolute HTTP(S) URLs, like `https://cdn.example.com/logo.png`, and protocol relative URLs, like `//cdn.example.com/logo.png`, are returned unchanged. `null` and white space values are treated as not set.
+ABP's built-in MVC themes resolve these URLs for the current request. `logo.png`, `/logo.png` and `~/logo.png` all mean the same application relative URL and include the `PathBase` of the request, so they keep working when the application is deployed to a non-root path, like an IIS virtual directory. A URL that already contains the path gets it twice. External URLs are used as they are. `LogoReverseUrl` is used by the themes that have a dark style, like LeptonX, and falls back to `LogoUrl`.
-If you render a branding URL in a custom MVC theme or view, resolve it with `Url.ResolveBrandingUrl(...)`.
+A logo that the project defines in its own CSS is not resolved. The LeptonX Lite startup templates set the logo that way, so remove that declaration to deploy them to a non-root path.
+
+To resolve a branding URL in a custom theme or view, use `Url.ResolveBrandingUrl(...)` with `@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Branding`, or `Url.ResolveBrandingCssUrl(...)` when it is rendered into `url('...')` in CSS.
> **Tip**: `IBrandingProvider` is used in every page refresh. For a multi-tenant application, you can return a tenant specific application name to customize it per tenant.
@@ -80,7 +82,7 @@ Both properties return `null` by default and follow the same URL rules as `LogoU
The active theme decides whether and where to use the compact logo. The LeptonX MVC theme enables its compact branding when `LogoIconUrl` is not empty: it uses the compact logo instead of the full logo in its branding areas and shows `AppName` next to it where there is room for both. Dark and dim styles use `LogoIconReverseUrl` and fall back to `LogoIconUrl` when it is not set. Themes that don't support `IBrandingLogoProvider` ignore these properties.
-> This URL resolution and the compact logo apply to the ASP.NET Core MVC / Razor Pages themes. The Blazor themes handle branding on their own.
+> The compact logo applies to the ASP.NET Core MVC / Razor Pages themes. The Blazor themes resolve the branding URLs by the same rules, see [Blazor UI: Branding](../blazor/branding.md).
## Overriding the Branding Area
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Branding/NavigationManagerBrandingExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Branding/NavigationManagerBrandingExtensions.cs
new file mode 100644
index 0000000000..51d4f04419
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Branding/NavigationManagerBrandingExtensions.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.AspNetCore.Components;
+using Volo.Abp.Ui.Branding;
+
+namespace Volo.Abp.AspNetCore.Components.Web.Theming.Branding;
+
+public static class NavigationManagerBrandingExtensions
+{
+ ///
+ /// Resolves a branding url of : "logo.svg", "/logo.svg" and
+ /// "~/logo.svg" all keep working under a non-root base path.
+ ///
+ public static string? ResolveBrandingUrl(this NavigationManager navigationManager, string? url)
+ {
+ Check.NotNull(navigationManager, nameof(navigationManager));
+
+ if (url.IsNullOrWhiteSpace())
+ {
+ return null;
+ }
+
+ var brandingUrl = url!.Trim();
+
+ if (BrandingUrlHelper.IsExternalUrl(brandingUrl))
+ {
+ return brandingUrl;
+ }
+
+ var applicationRelativeUrl = BrandingUrlHelper.RemoveApplicationRelativePrefix(brandingUrl);
+ var baseUri = new Uri(navigationManager.BaseUri);
+
+ // "/http://host/logo.svg" would silently lose its host when only the path is taken.
+ if (BrandingUrlHelper.IsExternalUrl(applicationRelativeUrl) ||
+ !Uri.TryCreate(baseUri, applicationRelativeUrl, out var absoluteUrl) ||
+ absoluteUrl.GetLeftPart(UriPartial.Authority) != baseUri.GetLeftPart(UriPartial.Authority))
+ {
+ return brandingUrl;
+ }
+
+ return absoluteUrl.PathAndQuery + absoluteUrl.Fragment;
+ }
+
+ ///
+ /// Same as , escaped for url('...') in css.
+ ///
+ public static string? ResolveBrandingCssUrl(this NavigationManager navigationManager, string? url)
+ {
+ var resolvedUrl = navigationManager.ResolveBrandingUrl(url);
+
+ return resolvedUrl == null ? null : BrandingUrlHelper.EscapeCssValue(resolvedUrl);
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Branding/UrlHelperBrandingExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Branding/UrlHelperBrandingExtensions.cs
index f652dac543..e817aadcbc 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Branding/UrlHelperBrandingExtensions.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Branding/UrlHelperBrandingExtensions.cs
@@ -1,16 +1,14 @@
using System;
using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.Ui.Branding;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Branding;
public static class UrlHelperBrandingExtensions
{
///
- /// Resolves a branding url of for the current request.
- /// "logo.svg", "/logo.svg" and "~/logo.svg" all mean the same application relative url and keep working
- /// under a non-root .
- /// External urls ("http://", "https://" and "//host/") are returned as they are.
- /// Returns null when is null or white space.
+ /// Resolves a branding url of : "logo.svg", "/logo.svg" and
+ /// "~/logo.svg" all keep working under a non-root .
///
public static string? ResolveBrandingUrl(this IUrlHelper urlHelper, string? url)
{
@@ -19,22 +17,31 @@ public static class UrlHelperBrandingExtensions
return null;
}
- if (IsExternalUrl(url!))
+ var brandingUrl = url!.Trim();
+
+ if (BrandingUrlHelper.IsExternalUrl(brandingUrl))
{
- return url;
+ return brandingUrl;
}
- var applicationRelativeUrl = url!.StartsWith("~/", StringComparison.Ordinal)
- ? url
- : "~/" + url.TrimStart('/');
+ var relativeUrl = BrandingUrlHelper.RemoveApplicationRelativePrefix(brandingUrl);
+
+ // "/http://host/logo.svg" would become a local path that does not exist.
+ if (BrandingUrlHelper.IsExternalUrl(relativeUrl))
+ {
+ return brandingUrl;
+ }
- return urlHelper.Content(applicationRelativeUrl);
+ return urlHelper.Content("~/" + relativeUrl);
}
- private static bool IsExternalUrl(string url)
+ ///
+ /// Same as , escaped for url('...') in css.
+ ///
+ public static string? ResolveBrandingCssUrl(this IUrlHelper urlHelper, string? url)
{
- return url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
- || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
- || url.StartsWith("//", StringComparison.Ordinal);
+ var resolvedUrl = urlHelper.ResolveBrandingUrl(url);
+
+ return resolvedUrl == null ? null : BrandingUrlHelper.EscapeCssValue(resolvedUrl);
}
}
diff --git a/framework/src/Volo.Abp.UI/Volo/Abp/Ui/Branding/BrandingUrlHelper.cs b/framework/src/Volo.Abp.UI/Volo/Abp/Ui/Branding/BrandingUrlHelper.cs
new file mode 100644
index 0000000000..16807175c8
--- /dev/null
+++ b/framework/src/Volo.Abp.UI/Volo/Abp/Ui/Branding/BrandingUrlHelper.cs
@@ -0,0 +1,69 @@
+using System;
+
+namespace Volo.Abp.Ui.Branding;
+
+public static class BrandingUrlHelper
+{
+ // "//host/" and any url with a scheme are used as they are, the others are application relative.
+ public static bool IsExternalUrl(string? url)
+ {
+ if (url.IsNullOrWhiteSpace())
+ {
+ return false;
+ }
+
+ var brandingUrl = url!.Trim();
+
+ return brandingUrl.StartsWith("//", StringComparison.Ordinal) || HasScheme(brandingUrl);
+ }
+
+ public static string RemoveApplicationRelativePrefix(string url)
+ {
+ return url.StartsWith("~/", StringComparison.Ordinal)
+ ? url.Substring(2)
+ : url.TrimStart('/');
+ }
+
+ // Rendered into url('...') inside a style element and must not be able to end either of them.
+ public static string EscapeCssValue(string url)
+ {
+ return url
+ .Replace("\\", "\\\\")
+ .Replace("'", "\\'")
+ .Replace("<", "%3C")
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty)
+ .Replace("\f", string.Empty);
+ }
+
+ // A broken value like "http://[" also has a scheme, so it is matched here instead of by Uri.TryCreate.
+ private static bool HasScheme(string url)
+ {
+ var schemeLength = url.IndexOf(':');
+ if (schemeLength < 1 || !IsLetter(url[0]))
+ {
+ return false;
+ }
+
+ for (var i = 1; i < schemeLength; i++)
+ {
+ var character = url[i];
+ if (!IsLetter(character) && !IsDigit(character) && character != '+' && character != '-' && character != '.')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool IsLetter(char character)
+ {
+ return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z');
+ }
+
+ private static bool IsDigit(char character)
+ {
+ return character >= '0' && character <= '9';
+ }
+}
diff --git a/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/Theming/Branding/NavigationManagerBrandingExtensions_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/Theming/Branding/NavigationManagerBrandingExtensions_Tests.cs
new file mode 100644
index 0000000000..eba70ffc2c
--- /dev/null
+++ b/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/Theming/Branding/NavigationManagerBrandingExtensions_Tests.cs
@@ -0,0 +1,143 @@
+using Microsoft.AspNetCore.Components;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.AspNetCore.Components.Web.Theming.Branding;
+
+public class NavigationManagerBrandingExtensions_Tests
+{
+ [Theory]
+ [InlineData("logo.svg")]
+ [InlineData("/logo.svg")]
+ [InlineData("~/logo.svg")]
+ public void Should_Treat_All_Local_Formats_As_Application_Relative(string url)
+ {
+ CreateNavigationManager("https://localhost/").ResolveBrandingUrl(url)
+ .ShouldBe("/logo.svg");
+
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl(url)
+ .ShouldBe("/myapp/logo.svg");
+ }
+
+ // Keep in sync with the same test of the mvc themes.
+ [Theory]
+ [InlineData("logo.svg", "/myapp/logo.svg")]
+ [InlineData("/logo.svg", "/myapp/logo.svg")]
+ [InlineData("~/logo.svg", "/myapp/logo.svg")]
+ [InlineData("images/logo.svg?v=42", "/myapp/images/logo.svg?v=42")]
+ [InlineData("images/logo.svg#brand", "/myapp/images/logo.svg#brand")]
+ [InlineData("https://cdn.example.com/logo.svg", "https://cdn.example.com/logo.svg")]
+ [InlineData("//cdn.example.com/logo.svg", "//cdn.example.com/logo.svg")]
+ [InlineData("data:image/svg+xml;base64,PHN2Zy8+", "data:image/svg+xml;base64,PHN2Zy8+")]
+ [InlineData("http://[", "http://[")]
+ [InlineData("https://", "https://")]
+ [InlineData("http://a b", "http://a b")]
+ [InlineData(" ~/logo.svg ", "/myapp/logo.svg")]
+ [InlineData(" https://cdn.example.com/logo.svg ", "https://cdn.example.com/logo.svg")]
+ [InlineData("/http://cdn.example.com/logo.svg", "/http://cdn.example.com/logo.svg")]
+ [InlineData("~/http://cdn.example.com/logo.svg", "~/http://cdn.example.com/logo.svg")]
+ public void Should_Resolve_The_Same_As_The_Mvc_Themes(string url, string expected)
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl(url)
+ .ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData("images/../logo.svg", "/myapp/logo.svg")]
+ [InlineData("images/../../logo.svg", "/logo.svg")]
+ public void Should_Resolve_A_Dot_Segment(string url, string expected)
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl(url).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void Should_Not_Resolve_A_Url_That_Points_To_Another_Host()
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl("/http://cdn.example.com/logo.svg")
+ .ShouldBe("/http://cdn.example.com/logo.svg");
+ }
+
+ [Fact]
+ public void Should_Escape_A_Form_Feed_In_Css()
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingCssUrl("data:image/svg+xml,\u000C")
+ .ShouldBe("data:image/svg+xml,%3Csvg/>");
+ }
+
+ [Fact]
+ public void Should_Keep_Query_And_Fragment()
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl("~/images/logo.svg?v=42")
+ .ShouldBe("/myapp/images/logo.svg?v=42");
+
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl("images/logo.svg#brand")
+ .ShouldBe("/myapp/images/logo.svg#brand");
+ }
+
+ [Theory]
+ [InlineData("http://cdn.example.com/logo.svg")]
+ [InlineData("https://cdn.example.com/logo.svg")]
+ [InlineData("//cdn.example.com/logo.svg")]
+ [InlineData("data:image/svg+xml;base64,PHN2Zy8+")]
+ [InlineData("http://[")]
+ [InlineData("https://")]
+ [InlineData("http://a b")]
+ public void Should_Return_External_Urls_As_They_Are(string url)
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl(url)
+ .ShouldBe(url);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Should_Return_Null_When_Url_Is_Empty(string? url)
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingUrl(url)
+ .ShouldBeNull();
+ }
+
+ [Fact]
+ public void Should_Escape_Css_Breaking_Characters()
+ {
+ CreateNavigationManager("https://localhost/myapp/")
+ .ResolveBrandingCssUrl("data:image/svg+xml,')")
+ .ShouldBe("data:image/svg+xml,%3Csvg/>\\')%3C/style>%3Cscript>alert(1)%3C/script>");
+ }
+
+ [Fact]
+ public void Should_Escape_Backslashes_And_Drop_Line_Breaks()
+ {
+ CreateNavigationManager("https://localhost/myapp/")
+ .ResolveBrandingCssUrl("data:image/svg+xml,a\\b\r\nc")
+ .ShouldBe("data:image/svg+xml,a\\\\bc");
+ }
+
+ [Fact]
+ public void Should_Resolve_And_Escape_Application_Relative_Urls()
+ {
+ CreateNavigationManager("https://localhost/myapp/")
+ .ResolveBrandingCssUrl("~/images/logo.svg")
+ .ShouldBe("/myapp/images/logo.svg");
+ }
+
+ [Fact]
+ public void Should_Return_Null_From_Css_Overload_When_Url_Is_Empty()
+ {
+ CreateNavigationManager("https://localhost/myapp/").ResolveBrandingCssUrl(" ").ShouldBeNull();
+ }
+
+ private static NavigationManager CreateNavigationManager(string baseUri)
+ {
+ return new TestNavigationManager(baseUri);
+ }
+
+ private class TestNavigationManager : NavigationManager
+ {
+ public TestNavigationManager(string baseUri)
+ {
+ Initialize(baseUri, baseUri);
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests/Volo/Abp/AspNetCore/Mvc/UI/Theme/Shared/Branding/UrlHelperBrandingExtensions_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests/Volo/Abp/AspNetCore/Mvc/UI/Theme/Shared/Branding/UrlHelperBrandingExtensions_Tests.cs
new file mode 100644
index 0000000000..72842ee52d
--- /dev/null
+++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests/Volo/Abp/AspNetCore/Mvc/UI/Theme/Shared/Branding/UrlHelperBrandingExtensions_Tests.cs
@@ -0,0 +1,110 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Routing;
+using Shouldly;
+using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Branding;
+using Xunit;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Branding;
+
+public class UrlHelperBrandingExtensions_Tests
+{
+ private readonly IUrlHelper _urlHelper;
+
+ public UrlHelperBrandingExtensions_Tests()
+ {
+ var httpContext = new DefaultHttpContext {
+ Request = { PathBase = "/myapp" }
+ };
+
+ _urlHelper = new UrlHelper(new ActionContext(httpContext, new RouteData(), new ActionDescriptor()));
+ }
+
+ [Theory]
+ [InlineData("logo.svg")]
+ [InlineData("/logo.svg")]
+ [InlineData("~/logo.svg")]
+ public void Should_Treat_All_Local_Formats_As_Application_Relative(string url)
+ {
+ _urlHelper.ResolveBrandingUrl(url).ShouldBe("/myapp/logo.svg");
+ }
+
+ // Keep in sync with the same test of the blazor themes.
+ [Theory]
+ [InlineData("logo.svg", "/myapp/logo.svg")]
+ [InlineData("/logo.svg", "/myapp/logo.svg")]
+ [InlineData("~/logo.svg", "/myapp/logo.svg")]
+ [InlineData("images/logo.svg?v=42", "/myapp/images/logo.svg?v=42")]
+ [InlineData("images/logo.svg#brand", "/myapp/images/logo.svg#brand")]
+ [InlineData("https://cdn.example.com/logo.svg", "https://cdn.example.com/logo.svg")]
+ [InlineData("//cdn.example.com/logo.svg", "//cdn.example.com/logo.svg")]
+ [InlineData("data:image/svg+xml;base64,PHN2Zy8+", "data:image/svg+xml;base64,PHN2Zy8+")]
+ [InlineData("http://[", "http://[")]
+ [InlineData("https://", "https://")]
+ [InlineData("http://a b", "http://a b")]
+ [InlineData(" ~/logo.svg ", "/myapp/logo.svg")]
+ [InlineData(" https://cdn.example.com/logo.svg ", "https://cdn.example.com/logo.svg")]
+ [InlineData("/http://cdn.example.com/logo.svg", "/http://cdn.example.com/logo.svg")]
+ [InlineData("~/http://cdn.example.com/logo.svg", "~/http://cdn.example.com/logo.svg")]
+ public void Should_Resolve_The_Same_As_The_Blazor_Themes(string url, string expected)
+ {
+ _urlHelper.ResolveBrandingUrl(url).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void Should_Keep_A_Dot_Segment_Of_An_Application_Relative_Url()
+ {
+ _urlHelper.ResolveBrandingUrl("images/../logo.svg").ShouldBe("/myapp/images/../logo.svg");
+ }
+
+ [Fact]
+ public void Should_Escape_A_Form_Feed_In_Css()
+ {
+ _urlHelper.ResolveBrandingCssUrl("data:image/svg+xml,\u000C")
+ .ShouldBe("data:image/svg+xml,%3Csvg/>");
+ }
+
+ [Fact]
+ public void Should_Keep_Query_And_Fragment()
+ {
+ _urlHelper.ResolveBrandingUrl("~/images/logo.svg?v=42").ShouldBe("/myapp/images/logo.svg?v=42");
+ _urlHelper.ResolveBrandingUrl("images/logo.svg#brand").ShouldBe("/myapp/images/logo.svg#brand");
+ }
+
+ [Theory]
+ [InlineData("http://cdn.example.com/logo.svg")]
+ [InlineData("https://cdn.example.com/logo.svg")]
+ [InlineData("//cdn.example.com/logo.svg")]
+ [InlineData("data:image/svg+xml;base64,PHN2Zy8+")]
+ [InlineData("http://[")]
+ [InlineData("https://")]
+ [InlineData("http://a b")]
+ public void Should_Return_External_Urls_As_They_Are(string url)
+ {
+ _urlHelper.ResolveBrandingUrl(url).ShouldBe(url);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Should_Return_Null_When_Url_Is_Empty(string? url)
+ {
+ _urlHelper.ResolveBrandingUrl(url).ShouldBeNull();
+ }
+
+ [Fact]
+ public void Should_Escape_Css_Breaking_Characters()
+ {
+ _urlHelper.ResolveBrandingCssUrl("data:image/svg+xml,')")
+ .ShouldBe("data:image/svg+xml,%3Csvg/>\\')%3C/style>");
+ }
+
+ [Fact]
+ public void Should_Resolve_And_Escape_Application_Relative_Urls()
+ {
+ _urlHelper.ResolveBrandingCssUrl("~/images/logo.svg").ShouldBe("/myapp/images/logo.svg");
+ }
+}
diff --git a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Branding/BrandingUrlHelper_Tests.cs b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Branding/BrandingUrlHelper_Tests.cs
new file mode 100644
index 0000000000..4524c4eeda
--- /dev/null
+++ b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Branding/BrandingUrlHelper_Tests.cs
@@ -0,0 +1,49 @@
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Ui.Branding;
+
+public class BrandingUrlHelper_Tests
+{
+ [Theory]
+ [InlineData("//cdn.example.com/logo.svg")]
+ [InlineData("https://cdn.example.com/logo.svg")]
+ [InlineData("data:image/svg+xml;base64,PHN2Zy8+")]
+ [InlineData("blob:1234")]
+ [InlineData(" https://cdn.example.com/logo.svg ")]
+ [InlineData("http://[")]
+ public void Should_Detect_External_Urls(string url)
+ {
+ BrandingUrlHelper.IsExternalUrl(url).ShouldBeTrue();
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("logo.svg")]
+ [InlineData("/logo.svg")]
+ [InlineData("~/logo.svg")]
+ [InlineData("/images/a:b.svg")]
+ [InlineData("1st:logo.svg")]
+ public void Should_Not_Detect_Other_Urls_As_External(string? url)
+ {
+ BrandingUrlHelper.IsExternalUrl(url).ShouldBeFalse();
+ }
+
+ [Theory]
+ [InlineData("~/images/logo.svg", "images/logo.svg")]
+ [InlineData("/images/logo.svg", "images/logo.svg")]
+ [InlineData("images/logo.svg", "images/logo.svg")]
+ public void Should_Remove_The_Application_Relative_Prefix(string url, string expected)
+ {
+ BrandingUrlHelper.RemoveApplicationRelativePrefix(url).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void Should_Escape_The_Characters_That_End_A_Css_Url_Or_A_Style_Element()
+ {
+ BrandingUrlHelper.EscapeCssValue("logo.svg\\')\r\n\f")
+ .ShouldBe("logo.svg\\\\\\')%3C/style>");
+ }
+}
diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/Branding.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/Branding.razor
index a0c91635a9..9ec1bc32c5 100644
--- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/Branding.razor
+++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/Branding.razor
@@ -1,9 +1,16 @@
+@using Volo.Abp.AspNetCore.Components.Web.Theming.Branding
@using Volo.Abp.Ui.Branding
@inject IBrandingProvider BrandingProvider
+@inject NavigationManager NavigationManager
+
+@{
+ var logoUrl = NavigationManager.ResolveBrandingUrl(BrandingProvider.LogoUrl);
+}
+
- @if (!BrandingProvider.LogoUrl.IsNullOrWhiteSpace())
+ @if (!logoUrl.IsNullOrWhiteSpace())
{
-
+
}
@BrandingProvider.AppName