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 |
|||
@inject IBrandingProvider BrandingProvider |
|||
@inject NavigationManager NavigationManager |
|||
|
|||
@{ |
|||
var logoUrl = NavigationManager.ResolveBrandingUrl(BrandingProvider.LogoUrl); |
|||
} |
|||
|
|||
<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 |
|||
</a> |
|||
|
|||
Loading…
Reference in new issue