2017-09-13 08:47:34 +02:00
|
|
|
|
using BTCPayServer.Authentication;
|
2017-09-22 18:31:29 +02:00
|
|
|
|
using BTCPayServer.Configuration;
|
2017-10-21 05:37:01 +02:00
|
|
|
|
using Microsoft.AspNetCore.Html;
|
2017-09-27 08:16:30 +02:00
|
|
|
|
using Microsoft.AspNetCore.Http;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2017-09-22 18:31:29 +02:00
|
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2017-10-21 05:37:01 +02:00
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Newtonsoft.Json.Serialization;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Text;
|
2017-10-27 05:27:15 +02:00
|
|
|
|
using System.Text.Encodings.Web;
|
2017-11-06 09:31:02 +01:00
|
|
|
|
using NBitcoin;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using NBXplorer;
|
|
|
|
|
using NBXplorer.Models;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
2018-01-06 18:16:42 +01:00
|
|
|
|
using BTCPayServer.Services.Wallets;
|
2018-01-07 18:36:41 +01:00
|
|
|
|
using System.IO;
|
2018-01-17 07:02:53 +01:00
|
|
|
|
using BTCPayServer.Logging;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2018-02-12 19:27:36 +01:00
|
|
|
|
using System.Net.WebSockets;
|
2018-02-18 18:38:03 +01:00
|
|
|
|
using BTCPayServer.Services.Invoices;
|
|
|
|
|
using NBitpayClient;
|
|
|
|
|
using BTCPayServer.Payments;
|
2018-04-03 04:50:41 +02:00
|
|
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
|
using BTCPayServer.Models;
|
|
|
|
|
using System.Security.Claims;
|
2018-04-18 11:23:39 +02:00
|
|
|
|
using System.Globalization;
|
2018-04-27 19:09:24 +02:00
|
|
|
|
using BTCPayServer.Services;
|
2018-04-29 13:32:43 +02:00
|
|
|
|
using BTCPayServer.Data;
|
2018-07-19 14:32:33 +02:00
|
|
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
2018-11-04 06:59:28 +01:00
|
|
|
|
using NBXplorer.DerivationStrategy;
|
2019-03-17 16:03:02 +01:00
|
|
|
|
using System.Net;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
|
|
|
|
namespace BTCPayServer
|
|
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
|
public static class Extensions
|
|
|
|
|
{
|
2018-04-18 11:23:39 +02:00
|
|
|
|
public static string PrettyPrint(this TimeSpan expiration)
|
|
|
|
|
{
|
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
|
|
|
if (expiration.Days >= 1)
|
|
|
|
|
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
|
|
|
|
if (expiration.Hours >= 1)
|
|
|
|
|
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
|
|
|
|
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
|
|
|
|
return builder.ToString();
|
|
|
|
|
}
|
2018-03-17 18:26:33 +01:00
|
|
|
|
public static decimal RoundUp(decimal value, int precision)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < precision; i++)
|
|
|
|
|
{
|
|
|
|
|
value = value * 10m;
|
|
|
|
|
}
|
|
|
|
|
value = Math.Ceiling(value);
|
|
|
|
|
for (int i = 0; i < precision; i++)
|
|
|
|
|
{
|
|
|
|
|
value = value / 10m;
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
2019-02-22 14:15:25 +01:00
|
|
|
|
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
|
|
|
|
|
{
|
|
|
|
|
if (value != 0m)
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
|
|
|
|
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
|
|
|
|
{
|
|
|
|
|
value = rounded;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
divisibility++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
2018-02-19 07:09:05 +01:00
|
|
|
|
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
2018-02-18 18:38:03 +01:00
|
|
|
|
{
|
2019-06-04 02:40:36 +02:00
|
|
|
|
return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType));
|
2018-02-18 18:38:03 +01:00
|
|
|
|
}
|
2018-02-12 19:27:36 +01:00
|
|
|
|
public static async Task CloseSocket(this WebSocket webSocket)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (webSocket.State == WebSocketState.Open)
|
|
|
|
|
{
|
|
|
|
|
CancellationTokenSource cts = new CancellationTokenSource();
|
|
|
|
|
cts.CancelAfter(5000);
|
|
|
|
|
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
finally { try { webSocket.Dispose(); } catch { } }
|
|
|
|
|
}
|
2018-01-06 10:57:56 +01:00
|
|
|
|
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
|
|
|
|
{
|
|
|
|
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-19 14:32:33 +02:00
|
|
|
|
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
|
|
|
|
{
|
|
|
|
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
|
|
|
|
}
|
|
|
|
|
public static bool SupportDropForeignKey(this DatabaseFacade facade)
|
|
|
|
|
{
|
|
|
|
|
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 09:29:48 +01:00
|
|
|
|
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
2017-11-06 09:31:02 +01:00
|
|
|
|
{
|
|
|
|
|
hashes = hashes.Distinct().ToArray();
|
|
|
|
|
var transactions = hashes
|
2018-01-11 09:29:48 +01:00
|
|
|
|
.Select(async o => await client.GetTransactionAsync(o, cts))
|
2017-11-06 09:31:02 +01:00
|
|
|
|
.ToArray();
|
|
|
|
|
await Task.WhenAll(transactions).ConfigureAwait(false);
|
|
|
|
|
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
|
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
public static string WithTrailingSlash(this string str)
|
|
|
|
|
{
|
2018-02-17 05:18:16 +01:00
|
|
|
|
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
2017-10-27 10:53:04 +02:00
|
|
|
|
return str;
|
|
|
|
|
return str + "/";
|
|
|
|
|
}
|
2019-03-01 05:20:21 +01:00
|
|
|
|
public static string WithStartingSlash(this string str)
|
|
|
|
|
{
|
|
|
|
|
if (str.StartsWith("/", StringComparison.InvariantCulture))
|
|
|
|
|
return str;
|
|
|
|
|
return $"/{str}";
|
|
|
|
|
}
|
2019-05-07 07:44:26 +02:00
|
|
|
|
public static string WithoutEndingSlash(this string str)
|
|
|
|
|
{
|
|
|
|
|
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
|
|
|
|
return str.Substring(0, str.Length - 1);
|
|
|
|
|
return str;
|
|
|
|
|
}
|
2017-09-27 08:16:30 +02:00
|
|
|
|
|
2018-07-12 10:38:21 +02:00
|
|
|
|
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
|
|
|
|
{
|
|
|
|
|
if (resp.HasStarted)
|
|
|
|
|
return;
|
|
|
|
|
resp.OnStarting(() =>
|
|
|
|
|
{
|
|
|
|
|
SetHeader(resp, name, value);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void SetHeader(this HttpResponse resp, string name, string value)
|
|
|
|
|
{
|
|
|
|
|
var existing = resp.Headers[name].FirstOrDefault();
|
|
|
|
|
if (existing != null && value == null)
|
|
|
|
|
resp.Headers.Remove(name);
|
|
|
|
|
else
|
|
|
|
|
resp.Headers[name] = value;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-04 06:59:28 +01:00
|
|
|
|
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
|
|
|
|
|
{
|
|
|
|
|
if (IsSegwitCore(derivationStrategyBase))
|
|
|
|
|
return true;
|
|
|
|
|
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
|
|
|
|
|
{
|
|
|
|
|
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
|
|
|
|
|
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-04 07:28:11 +02:00
|
|
|
|
public static bool IsLocalNetwork(string server)
|
|
|
|
|
{
|
|
|
|
|
if (server == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(server));
|
|
|
|
|
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
|
|
|
|
|
{
|
|
|
|
|
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
|
|
|
|
|
}
|
|
|
|
|
if(IPAddress.TryParse(server, out var ip))
|
|
|
|
|
{
|
2019-06-12 10:40:49 +02:00
|
|
|
|
return ip.IsLocal() || ip.IsRFC1918();
|
2019-04-04 07:28:11 +02:00
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-17 13:28:47 +01:00
|
|
|
|
public static bool IsOnion(this HttpRequest request)
|
|
|
|
|
{
|
2019-03-18 10:52:19 +01:00
|
|
|
|
if (request?.Host.Host == null)
|
|
|
|
|
return false;
|
2019-03-17 13:28:47 +01:00
|
|
|
|
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
public static string GetAbsoluteRoot(this HttpRequest request)
|
|
|
|
|
{
|
|
|
|
|
return string.Concat(
|
|
|
|
|
request.Scheme,
|
|
|
|
|
"://",
|
|
|
|
|
request.Host.ToUriComponent(),
|
|
|
|
|
request.PathBase.ToUriComponent());
|
|
|
|
|
}
|
2017-09-22 18:31:29 +02:00
|
|
|
|
|
2019-05-24 08:44:23 +02:00
|
|
|
|
public static Uri GetAbsoluteRootUri(this HttpRequest request)
|
|
|
|
|
{
|
|
|
|
|
return new Uri(request.GetAbsoluteRoot());
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-01 17:16:16 +02:00
|
|
|
|
public static string GetCurrentUrl(this HttpRequest request)
|
|
|
|
|
{
|
|
|
|
|
return string.Concat(
|
|
|
|
|
request.Scheme,
|
|
|
|
|
"://",
|
|
|
|
|
request.Host.ToUriComponent(),
|
|
|
|
|
request.PathBase.ToUriComponent(),
|
|
|
|
|
request.Path.ToUriComponent());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetCurrentPath(this HttpRequest request)
|
|
|
|
|
{
|
|
|
|
|
return string.Concat(
|
|
|
|
|
request.PathBase.ToUriComponent(),
|
|
|
|
|
request.Path.ToUriComponent());
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-21 05:33:26 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
|
|
|
|
/// If 'toto' and RootPath is empty returns '/toto'
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="request"></param>
|
|
|
|
|
/// <param name="path"></param>
|
|
|
|
|
/// <returns></returns>
|
2018-12-07 10:42:39 +01:00
|
|
|
|
public static string GetRelativePath(this HttpRequest request, string path)
|
|
|
|
|
{
|
|
|
|
|
if (path.Length > 0 && path[0] != '/')
|
|
|
|
|
path = $"/{path}";
|
|
|
|
|
return string.Concat(
|
|
|
|
|
request.PathBase.ToUriComponent(),
|
|
|
|
|
path);
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-21 05:31:02 +01:00
|
|
|
|
/// <summary>
|
2018-12-21 05:33:26 +01:00
|
|
|
|
/// If 'https://example.com/toto' returns 'https://example.com/toto'
|
|
|
|
|
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
|
|
|
|
/// If 'toto' and RootPath is empty returns '/toto'
|
2018-12-21 05:31:02 +01:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="request"></param>
|
|
|
|
|
/// <param name="path"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
|
|
|
|
|
{
|
2019-03-08 15:37:33 +01:00
|
|
|
|
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
|
|
|
|
|
uri.IsAbsoluteUri)
|
2018-12-21 05:31:02 +01:00
|
|
|
|
return path;
|
2019-03-08 15:37:33 +01:00
|
|
|
|
|
2018-12-21 05:31:02 +01:00
|
|
|
|
if (path.Length > 0 && path[0] != '/')
|
|
|
|
|
path = $"/{path}";
|
|
|
|
|
return string.Concat(
|
|
|
|
|
request.PathBase.ToUriComponent(),
|
|
|
|
|
path);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-29 13:48:17 +02:00
|
|
|
|
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
|
|
|
|
{
|
2018-07-08 15:20:59 +02:00
|
|
|
|
bool isRelative =
|
2018-04-29 13:50:54 +02:00
|
|
|
|
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
|
|
|
|
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
|
|
|
|
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
2018-04-29 13:48:17 +02:00
|
|
|
|
}
|
2019-03-01 05:20:21 +01:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Will return an absolute URL.
|
|
|
|
|
/// If `relativeOrAsbolute` is absolute, returns it.
|
|
|
|
|
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="request"></param>
|
|
|
|
|
/// <param name="relativeOrAbsolte"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
|
2019-02-28 15:01:25 +01:00
|
|
|
|
{
|
2019-03-01 05:20:21 +01:00
|
|
|
|
if (relativeOrAbsolute == null)
|
2019-02-28 15:01:25 +01:00
|
|
|
|
{
|
2019-03-01 05:20:21 +01:00
|
|
|
|
return new Uri(string.Concat(
|
|
|
|
|
request.Scheme,
|
|
|
|
|
"://",
|
|
|
|
|
request.Host.ToUriComponent()), UriKind.Absolute);
|
2019-02-28 15:01:25 +01:00
|
|
|
|
}
|
2019-03-01 05:20:21 +01:00
|
|
|
|
if (relativeOrAbsolute.IsAbsoluteUri)
|
|
|
|
|
return relativeOrAbsolute;
|
|
|
|
|
return new Uri(string.Concat(
|
2019-02-28 15:01:25 +01:00
|
|
|
|
request.Scheme,
|
|
|
|
|
"://",
|
2019-03-01 05:20:21 +01:00
|
|
|
|
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
|
2019-02-28 15:01:25 +01:00
|
|
|
|
}
|
2018-04-29 13:48:17 +02:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
|
|
|
|
{
|
|
|
|
|
services.Configure<BTCPayServerOptions>(o =>
|
|
|
|
|
{
|
|
|
|
|
o.LoadArgs(conf);
|
|
|
|
|
});
|
|
|
|
|
return services;
|
|
|
|
|
}
|
2017-09-22 18:31:29 +02:00
|
|
|
|
|
2018-04-27 19:09:24 +02:00
|
|
|
|
public static string GetSIN(this ClaimsPrincipal principal)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-04-27 19:09:24 +02:00
|
|
|
|
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2017-10-21 05:37:01 +02:00
|
|
|
|
|
2018-04-27 19:51:20 +02:00
|
|
|
|
public static string GetStoreId(this ClaimsPrincipal principal)
|
|
|
|
|
{
|
|
|
|
|
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-29 13:32:43 +02:00
|
|
|
|
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
|
|
|
|
|
{
|
|
|
|
|
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool GetIsBitpayAPI(this HttpContext ctx)
|
|
|
|
|
{
|
|
|
|
|
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
|
|
|
|
|
obj is bool b && b;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-30 15:28:00 +02:00
|
|
|
|
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
|
|
|
|
|
{
|
|
|
|
|
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 11:40:47 +02:00
|
|
|
|
public static bool TryGetBitpayAuth(this HttpContext ctx, out (string Signature, String Id, String Authorization) result)
|
2018-04-30 15:28:00 +02:00
|
|
|
|
{
|
2019-06-11 11:40:47 +02:00
|
|
|
|
if (ctx.Items.TryGetValue("BitpayAuth", out object obj))
|
|
|
|
|
{
|
|
|
|
|
result = ((string Signature, String Id, String Authorization))obj;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
result = default;
|
|
|
|
|
return false;
|
2018-04-30 15:28:00 +02:00
|
|
|
|
}
|
2018-04-29 13:48:17 +02:00
|
|
|
|
|
2018-04-29 13:32:43 +02:00
|
|
|
|
public static StoreData GetStoreData(this HttpContext ctx)
|
|
|
|
|
{
|
|
|
|
|
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
|
|
|
|
|
}
|
|
|
|
|
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
|
|
|
|
|
{
|
|
|
|
|
ctx.Items["BTCPAY.STOREDATA"] = storeData;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
|
|
|
|
public static string ToJson(this object o)
|
|
|
|
|
{
|
|
|
|
|
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
2019-02-27 12:25:13 +01:00
|
|
|
|
|
|
|
|
|
public static string TrimEnd(this string input, string suffixToRemove,
|
|
|
|
|
StringComparison comparisonType) {
|
|
|
|
|
|
|
|
|
|
if (input != null && suffixToRemove != null
|
|
|
|
|
&& input.EndsWith(suffixToRemove, comparisonType)) {
|
|
|
|
|
return input.Substring(0, input.Length - suffixToRemove.Length);
|
|
|
|
|
}
|
|
|
|
|
else return input;
|
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
}
|