Add suggestion list for currency inputs (#3347)

* Move tagHelpers in their own directory

* Add suggestion list for currency inputs
This commit is contained in:
Nicolas Dorier 2022-01-24 20:00:13 +09:00 committed by GitHub
parent 30db0cd4f4
commit 11d6588249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 280 additions and 224 deletions

View file

@ -143,6 +143,8 @@ namespace BTCPayServer.Services.Rates
return currencies;
}
public IEnumerable<CurrencyData> Currencies => _Currencies.Values;
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
ArgumentNullException.ThrowIfNull(currency);

View file

@ -1,51 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.Security
{
[HtmlTargetElement(Attributes = nameof(Permission))]
public class PermissionTagHelper : TagHelper
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<PermissionTagHelper> _logger;
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public string Permission { get; set; }
public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (string.IsNullOrEmpty(Permission))
{
return;
}
var key = $"{Permission}_{PermissionResource}";
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var cachedResult))
{
var result = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
PermissionResource,
Permission);
cachedResult = result;
_httpContextAccessor.HttpContext.Items.Add(key, result);
}
if (!((AuthorizationResult)cachedResult).Succeeded)
{
output.SuppressOutput();
}
}
}
}

View file

@ -1,168 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using NBitcoin;
using NBitcoin.Crypto;
namespace BTCPayServer.TagHelpers
{
[HtmlTargetElement("srv-model")]
public class SrvModel : TagHelper
{
private readonly Safe _safe;
private readonly ContentSecurityPolicies _csp;
public SrvModel(Safe safe, ContentSecurityPolicies csp)
{
_safe = safe;
_csp = csp;
}
public string VarName { get; set; } = "srvModel";
public object Model { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "script";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.Add(new TagHelperAttribute("type", "text/javascript"));
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
output.Content.SetHtmlContent($"var {VarName} = {_safe.Json(Model)};");
}
}
/// <summary>
/// Add a nonce-* so the inline-script can pass CSP rule when they are rendered server-side
/// </summary>
[HtmlTargetElement("script")]
public class CSPInlineScriptTagHelper : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPInlineScriptTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (output.Attributes.ContainsName("src"))
return;
if (output.Attributes.TryGetAttribute("type", out var attr))
{
if (attr.Value?.ToString() != "text/javascript")
return;
}
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
}
}
/// <summary>
/// Add 'unsafe-hashes' and sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement(Attributes = "onclick")]
[HtmlTargetElement(Attributes = "onkeypress")]
[HtmlTargetElement(Attributes = "onchange")]
[HtmlTargetElement(Attributes = "onsubmit")]
public class CSPEventTagHelper : TagHelper
{
public const string EventNames = "onclick,onkeypress,onchange,onsubmit";
private readonly ContentSecurityPolicies _csp;
readonly static HashSet<string> EventSet = EventNames.Split(',')
.ToHashSet();
public CSPEventTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
foreach (var attr in output.Attributes)
{
var n = attr.Name.ToLowerInvariant();
if (EventSet.Contains(n))
{
_csp.AllowUnsafeHashes(attr.Value.ToString());
}
}
}
}
/// <summary>
/// Add sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement("template", Attributes = "csp-allow")]
public class CSPTemplate : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPTemplate(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
var childContent = await output.GetChildContentAsync();
var content = childContent.GetContent();
_csp.AllowInline(content);
}
}
/// <summary>
/// Add sha256- to allow inline event handlers in a:href=javascript:
/// </summary>
[HtmlTargetElement("a", Attributes = "csp-allow")]
public class CSPA : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPA(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
if (output.Attributes.TryGetAttribute("href", out var attr))
{
var v = attr.Value.ToString();
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
{
_csp.AllowUnsafeHashes(v);
}
}
}
}
// Make sure that <svg><use href=/ are correctly working if rootpath is present
[HtmlTargetElement("use", Attributes = "href")]
public class SVGUse : UrlResolutionTagHelper
{
private readonly IFileVersionProvider _fileVersionProvider;
public SVGUse(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, IFileVersionProvider fileVersionProvider):base(urlHelperFactory, htmlEncoder)
{
_fileVersionProvider = fileVersionProvider;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var attr = output.Attributes["href"].Value.ToString();
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
output.Attributes.SetAttribute("href", attr);
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.TagHelpers;
/// <summary>
/// Add sha256- to allow inline event handlers in a:href=javascript:
/// </summary>
[HtmlTargetElement("a", Attributes = "csp-allow")]
public class CSPA : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPA(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
if (output.Attributes.TryGetAttribute("href", out var attr))
{
var v = attr.Value.ToString();
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
{
_csp.AllowUnsafeHashes(v);
}
}
}
}

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.TagHelpers;
/// <summary>
/// Add 'unsafe-hashes' and sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement(Attributes = "onclick")]
[HtmlTargetElement(Attributes = "onkeypress")]
[HtmlTargetElement(Attributes = "onchange")]
[HtmlTargetElement(Attributes = "onsubmit")]
public class CSPEventTagHelper : TagHelper
{
public const string EventNames = "onclick,onkeypress,onchange,onsubmit";
private readonly ContentSecurityPolicies _csp;
readonly static HashSet<string> EventSet = EventNames.Split(',')
.ToHashSet();
public CSPEventTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
foreach (var attr in output.Attributes)
{
var n = attr.Name.ToLowerInvariant();
if (EventSet.Contains(n))
{
_csp.AllowUnsafeHashes(attr.Value.ToString());
}
}
}
}

View file

@ -0,0 +1,33 @@
using BTCPayServer.Security;
using Microsoft.AspNetCore.Razor.TagHelpers;
using NBitcoin;
namespace BTCPayServer.TagHelpers;
/// <summary>
/// Add a nonce-* so the inline-script can pass CSP rule when they are rendered server-side
/// </summary>
[HtmlTargetElement("script")]
public class CSPInlineScriptTagHelper : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPInlineScriptTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (output.Attributes.ContainsName("src"))
return;
if (output.Attributes.TryGetAttribute("type", out var attr))
{
if (attr.Value?.ToString() != "text/javascript")
return;
}
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
}
}

View file

@ -0,0 +1,24 @@
using System.Threading.Tasks;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.TagHelpers;
/// <summary>
/// Add sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement("template", Attributes = "csp-allow")]
public class CSPTemplate : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPTemplate(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
var childContent = await output.GetChildContentAsync();
var content = childContent.GetContent();
_csp.AllowInline(content);
}
}

View file

@ -0,0 +1,44 @@
using BTCPayServer.Services.Rates;
using System.Linq;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;
namespace BTCPayServer.TagHelpers
{
[HtmlTargetElement("input", Attributes = "currency-selection")]
public class CurrenciesSuggestionsTagHelper : TagHelper
{
private readonly CurrencyNameTable _currencies;
public CurrenciesSuggestionsTagHelper(CurrencyNameTable currencies)
{
_currencies = currencies;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("currency-selection");
output.PostElement.AppendHtml("<datalist id=\"currency-selection-suggestion\">");
var currencies = _currencies.Currencies.Where(c => !c.Crypto).Select(c => c.Code).OrderBy(c => c).ToList();
int pos = 0;
InsertAt(currencies, "BTC", pos++);
InsertAt(currencies, "SATS", pos++);
InsertAt(currencies, "USD", pos++);
InsertAt(currencies, "EUR", pos++);
InsertAt(currencies, "JPY", pos++);
InsertAt(currencies, "CNY", pos++);
foreach (var curr in currencies)
{
output.PostElement.AppendHtml($"<option value=\"{curr}\">");
}
output.PostElement.AppendHtml("</datalist>");
output.Attributes.Add("list", "currency-selection-suggestion");
base.Process(context, output);
}
private void InsertAt(List<string> currencies, string curr, int idx)
{
currencies.Remove(curr);
currencies.Insert(idx, curr);
}
}
}

View file

@ -0,0 +1,50 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.TagHelpers;
[HtmlTargetElement(Attributes = nameof(Permission))]
public class PermissionTagHelper : TagHelper
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<PermissionTagHelper> _logger;
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public string Permission { get; set; }
public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (string.IsNullOrEmpty(Permission))
{
return;
}
var key = $"{Permission}_{PermissionResource}";
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var cachedResult))
{
var result = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
PermissionResource,
Permission);
cachedResult = result;
_httpContextAccessor.HttpContext.Items.Add(key, result);
}
if (!((AuthorizationResult)cachedResult).Succeeded)
{
output.SuppressOutput();
}
}
}

View file

@ -0,0 +1,26 @@
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.TagHelpers;
// Make sure that <svg><use href=/ are correctly working if rootpath is present
[HtmlTargetElement("use", Attributes = "href")]
public class SVGUse : UrlResolutionTagHelper
{
private readonly IFileVersionProvider _fileVersionProvider;
public SVGUse(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, IFileVersionProvider fileVersionProvider) : base(urlHelperFactory, htmlEncoder)
{
_fileVersionProvider = fileVersionProvider;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var attr = output.Attributes["href"].Value.ToString();
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
output.Attributes.SetAttribute("href", attr);
}
}

View file

@ -0,0 +1,30 @@
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Razor.TagHelpers;
using NBitcoin;
namespace BTCPayServer.TagHelpers;
[HtmlTargetElement("srv-model")]
public class SrvModel : TagHelper
{
private readonly Safe _safe;
private readonly ContentSecurityPolicies _csp;
public SrvModel(Safe safe, ContentSecurityPolicies csp)
{
_safe = safe;
_csp = csp;
}
public string VarName { get; set; } = "srvModel";
public object Model { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "script";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.Add(new TagHelperAttribute("type", "text/javascript"));
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
output.Content.SetHtmlContent($"var {VarName} = {_safe.Json(Model)};");
}
}

View file

@ -27,7 +27,7 @@
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" placeholder="Use store's default settings" />
<input asp-for="Currency" currency-selection class="form-control" placeholder="Use store's default settings" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>

View file

@ -55,7 +55,7 @@
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" />
<input asp-for="Currency" currency-selection class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>

View file

@ -1,4 +1,4 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Views.Stores
@model BTCPayServer.Models.WalletViewModels.NewPullPaymentModel
@{
@ -27,7 +27,7 @@
</div>
<div class="form-group col-4">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control"/>
<input asp-for="Currency" currency-selection class="form-control"/>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>

View file

@ -14,7 +14,7 @@
<h3 class="mb-3">Payment</h3>
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" style="max-width:10ch;" />
<input asp-for="DefaultCurrency" currency-selection class="form-control" style="max-width:10ch;" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
<div class="form-group d-flex align-items-center">