mirror of https://github.com/abpframework/abp.git
committed by
GitHub
9 changed files with 467 additions and 20 deletions
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Resolves a branding url of <see cref="IBrandingProvider"/>: "logo.svg", "/logo.svg" and
|
||||
|
/// "~/logo.svg" all keep working under a non-root base path.
|
||||
|
/// </summary>
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Same as <see cref="ResolveBrandingUrl"/>, escaped for url('...') in css.
|
||||
|
/// </summary>
|
||||
|
public static string? ResolveBrandingCssUrl(this NavigationManager navigationManager, string? url) |
||||
|
{ |
||||
|
var resolvedUrl = navigationManager.ResolveBrandingUrl(url); |
||||
|
|
||||
|
return resolvedUrl == null ? null : BrandingUrlHelper.EscapeCssValue(resolvedUrl); |
||||
|
} |
||||
|
} |
||||
@ -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'; |
||||
|
} |
||||
|
} |
||||
@ -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<svg/>") |
||||
|
.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,<svg/>')</style><script>alert(1)</script>") |
||||
|
.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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<svg/>") |
||||
|
.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,<svg/>')</style>") |
||||
|
.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"); |
||||
|
} |
||||
|
} |
||||
@ -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</style>") |
||||
|
.ShouldBe("logo.svg\\\\\\')%3C/style>"); |
||||
|
} |
||||
|
} |
||||
@ -1,9 +1,16 @@ |
|||||
|
@using Volo.Abp.AspNetCore.Components.Web.Theming.Branding |
||||
@using Volo.Abp.Ui.Branding |
@using Volo.Abp.Ui.Branding |
||||
@inject IBrandingProvider BrandingProvider |
@inject IBrandingProvider BrandingProvider |
||||
|
@inject NavigationManager NavigationManager |
||||
|
|
||||
|
@{ |
||||
|
var logoUrl = NavigationManager.ResolveBrandingUrl(BrandingProvider.LogoUrl); |
||||
|
} |
||||
|
|
||||
<a class="navbar-brand" href=""> |
<a class="navbar-brand" href=""> |
||||
@if (!BrandingProvider.LogoUrl.IsNullOrWhiteSpace()) |
@if (!logoUrl.IsNullOrWhiteSpace()) |
||||
{ |
{ |
||||
<img src="@BrandingProvider.LogoUrl.TrimStart('/', '~')" alt="@BrandingProvider.AppName"> |
<img src="@logoUrl" alt="@BrandingProvider.AppName"> |
||||
} |
} |
||||
@BrandingProvider.AppName |
@BrandingProvider.AppName |
||||
</a> |
</a> |
||||
|
|||||
Loading…
Reference in new issue