C# formatting
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using GameList.Data;
|
||||
using GameList.Domain;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
|
||||
@@ -11,7 +9,8 @@ internal static class EndpointHelpers
|
||||
{
|
||||
public static async Task<Player?> GetAuthenticatedPlayer(HttpContext ctx, AppDbContext db)
|
||||
{
|
||||
if (ctx?.User?.Identity?.IsAuthenticated != true) return null;
|
||||
if (ctx.User.Identity?.IsAuthenticated != true)
|
||||
return null;
|
||||
|
||||
if (ctx.Items.TryGetValue(nameof(Player), out var cached) && cached is Player cachedPlayer)
|
||||
return cachedPlayer;
|
||||
@@ -38,7 +37,8 @@ internal static class EndpointHelpers
|
||||
public static async Task<Phase> GetPhase(AppDbContext db, Guid playerId)
|
||||
{
|
||||
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||
if (player is null) return Phase.Suggest;
|
||||
if (player is null)
|
||||
return Phase.Suggest;
|
||||
|
||||
var state = await db.AppState.FirstAsync();
|
||||
|
||||
@@ -68,6 +68,7 @@ internal static class EndpointHelpers
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return player.CurrentPhase;
|
||||
}
|
||||
|
||||
@@ -75,58 +76,67 @@ internal static class EndpointHelpers
|
||||
Results.BadRequest(new { error = $"This endpoint is available in the {required} phase. Your current phase is {current}." });
|
||||
|
||||
public static string? TrimTo(string? input, int max) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? null
|
||||
: input.Trim() is var t && t.Length > 0
|
||||
? t[..Math.Min(t.Length, max)]
|
||||
: null;
|
||||
string.IsNullOrWhiteSpace(input) ? null : input.Trim() is { Length: > 0 } t ? t[..Math.Min(t.Length, max)] : null;
|
||||
|
||||
public static bool IsValidImageUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return true; // empty is acceptable
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
|
||||
if (uri.Scheme is not ("http" or "https")) return false;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return true; // empty is acceptable
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
if (uri.Scheme is not ("http" or "https"))
|
||||
return false;
|
||||
|
||||
var path = uri.AbsolutePath.ToLowerInvariant();
|
||||
return path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg")
|
||||
|| path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif");
|
||||
return path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif");
|
||||
}
|
||||
|
||||
public static async Task<bool> IsReachableImageAsync(string? url, IHttpClientFactory httpFactory, HttpMessageHandler? handler = null, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return true;
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
|
||||
if (uri.Scheme is not ("http" or "https")) return false;
|
||||
if (!await IsSafePublicHostAsync(uri, httpFactory, ct)) return false;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return true;
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
if (uri.Scheme is not ("http" or "https"))
|
||||
return false;
|
||||
if (!await IsSafePublicHostAsync(uri, ct))
|
||||
return false;
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(3));
|
||||
|
||||
var client = handler is null
|
||||
? httpFactory.CreateClient("imageValidation")
|
||||
: new HttpClient(handler, disposeHandler: false);
|
||||
var client = handler is null ? httpFactory.CreateClient("imageValidation") : new HttpClient(handler, disposeHandler: false);
|
||||
|
||||
try
|
||||
{
|
||||
using var head = new HttpRequestMessage(HttpMethod.Head, uri);
|
||||
var headResp = await client.SendAsync(head, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (headResp.IsSuccessStatusCode && headResp.StatusCode is not System.Net.HttpStatusCode.Redirect)
|
||||
if (headResp is { IsSuccessStatusCode: true, StatusCode: not System.Net.HttpStatusCode.Redirect })
|
||||
{
|
||||
if (headResp.Content.Headers.ContentLength is long headLen && headLen > MaxImageBytes) return false;
|
||||
if (headResp.Content.Headers.ContentLength is > MaxImageBytes)
|
||||
return false;
|
||||
|
||||
var ctHeader = headResp.Content.Headers.ContentType?.MediaType;
|
||||
if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { /* fallback */ }
|
||||
catch
|
||||
{
|
||||
/* fallback */
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var get = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
get.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 1023);
|
||||
var resp = await client.SendAsync(get, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (!resp.IsSuccessStatusCode) return false;
|
||||
if (resp.StatusCode is System.Net.HttpStatusCode.Redirect) return false;
|
||||
if (resp.Content.Headers.ContentLength is long len && len > MaxImageBytes) return false;
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
return false;
|
||||
if (resp.StatusCode is System.Net.HttpStatusCode.Redirect)
|
||||
return false;
|
||||
if (resp.Content.Headers.ContentLength is > MaxImageBytes)
|
||||
return false;
|
||||
|
||||
var ctHeader = resp.Content.Headers.ContentType?.MediaType;
|
||||
if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -137,11 +147,16 @@ internal static class EndpointHelpers
|
||||
var read = await stream.ReadAsync(rented, 0, rented.Length, cts.Token);
|
||||
var sig = new ReadOnlySpan<byte>(rented, 0, read);
|
||||
|
||||
if (IsMagic(sig, "PNG")) return true;
|
||||
if (IsMagic(sig, new byte[] { 0xFF, 0xD8 })) return true; // JPEG
|
||||
if (IsMagic(sig, "GIF8")) return true;
|
||||
if (IsRiffWithTag(sig, "WEBP")) return true;
|
||||
if (ContainsFtyp(sig, "avif")) return true;
|
||||
if (IsMagic(sig, "PNG"))
|
||||
return true;
|
||||
if (IsMagic(sig, [0xFF, 0xD8]))
|
||||
return true; // JPEG
|
||||
if (IsMagic(sig, "GIF8"))
|
||||
return true;
|
||||
if (IsRiffWithTag(sig, "WEBP"))
|
||||
return true;
|
||||
if (ContainsFtyp(sig, "avif"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -153,7 +168,7 @@ internal static class EndpointHelpers
|
||||
|
||||
private const long MaxImageBytes = 5 * 1024 * 1024; // 5 MB guard
|
||||
|
||||
private static async Task<bool> IsSafePublicHostAsync(Uri uri, IHttpClientFactory httpFactory, CancellationToken ct)
|
||||
private static async Task<bool> IsSafePublicHostAsync(Uri uri, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -163,8 +178,10 @@ internal static class EndpointHelpers
|
||||
var addresses = await System.Net.Dns.GetHostAddressesAsync(host, ct);
|
||||
foreach (var ip in addresses)
|
||||
{
|
||||
if (System.Net.IPAddress.IsLoopback(ip)) return false;
|
||||
if (IsPrivate(ip)) return false;
|
||||
if (System.Net.IPAddress.IsLoopback(ip))
|
||||
return false;
|
||||
if (IsPrivate(ip))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -187,11 +204,11 @@ internal static class EndpointHelpers
|
||||
var bytes = ip.GetAddressBytes();
|
||||
return bytes[0] switch
|
||||
{
|
||||
10 => true,
|
||||
10 => true,
|
||||
172 when bytes[1] >= 16 && bytes[1] <= 31 => true,
|
||||
192 when bytes[1] == 168 => true,
|
||||
127 => true,
|
||||
_ => false
|
||||
192 when bytes[1] == 168 => true,
|
||||
127 => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,26 +230,37 @@ internal static class EndpointHelpers
|
||||
|
||||
private static bool IsRiffWithTag(ReadOnlySpan<byte> data, string tag)
|
||||
{
|
||||
if (data.Length < 12) return false;
|
||||
var riff = System.Text.Encoding.ASCII.GetBytes("RIFF");
|
||||
if (!data.StartsWith(riff)) return false;
|
||||
if (data.Length < 12)
|
||||
return false;
|
||||
|
||||
var riff = "RIFF"u8.ToArray();
|
||||
if (!data.StartsWith(riff))
|
||||
return false;
|
||||
|
||||
var tagBytes = System.Text.Encoding.ASCII.GetBytes(tag);
|
||||
return data[8..].StartsWith(tagBytes);
|
||||
}
|
||||
|
||||
private static bool ContainsFtyp(ReadOnlySpan<byte> data, string brand)
|
||||
{
|
||||
if (data.Length < 12) return false;
|
||||
var ftyp = System.Text.Encoding.ASCII.GetBytes("ftyp");
|
||||
if (!data[4..].StartsWith(ftyp)) return false;
|
||||
if (data.Length < 12)
|
||||
return false;
|
||||
|
||||
var ftyp = "ftyp"u8.ToArray();
|
||||
if (!data[4..].StartsWith(ftyp))
|
||||
return false;
|
||||
|
||||
var brandBytes = System.Text.Encoding.ASCII.GetBytes(brand);
|
||||
return data[8..].StartsWith(brandBytes);
|
||||
}
|
||||
|
||||
public static bool IsValidHttpUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return true; // empty is allowed
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return true; // empty is allowed
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
|
||||
return uri.Scheme is "http" or "https";
|
||||
}
|
||||
|
||||
@@ -257,6 +285,7 @@ internal static class EndpointHelpers
|
||||
{
|
||||
roots[id] = FindRootId(id, parentMap);
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
@@ -265,7 +294,7 @@ internal static class EndpointHelpers
|
||||
var current = suggestionId;
|
||||
var visited = new HashSet<int>();
|
||||
|
||||
while (parentMap.TryGetValue(current, out var parent) && parent is int p && !visited.Contains(p))
|
||||
while (parentMap.TryGetValue(current, out var parent) && parent is { } p && !visited.Contains(p))
|
||||
{
|
||||
visited.Add(current);
|
||||
current = p;
|
||||
@@ -276,7 +305,9 @@ internal static class EndpointHelpers
|
||||
|
||||
public static List<int> LinkedIdsFor(int suggestionId, IReadOnlyDictionary<int, int> rootIndex)
|
||||
{
|
||||
if (!rootIndex.TryGetValue(suggestionId, out var root)) return new();
|
||||
if (!rootIndex.TryGetValue(suggestionId, out var root))
|
||||
return [];
|
||||
|
||||
return rootIndex.Where(kv => kv.Value == root).Select(kv => kv.Key).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user