Skip to content

Commit 4fa6079

Browse files
committed
feat: replace lang in index.html
1 parent 5f26b36 commit 4fa6079

File tree

6 files changed

+95
-73
lines changed

6 files changed

+95
-73
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Obj/
2626

2727
# Visual Studio 2015 cache/options directory
2828
.vs/
29-
/wwwroot/dist/
29+
**/wwwroot/dist/
30+
**/wwwroot/index.html
3031

3132
# MSTest test Results
3233
[Tt]est[Rr]esult*/

src/GZCTF/ClientApp/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html lang="en-us">
2+
<html lang="%lang%">
33
<head>
44
<meta charset="UTF-8" />
55
<meta content="width=device-width, initial-scale=1.0" name="viewport" />

src/GZCTF/Extensions/ConfigurationExtension.cs

+42-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.Net.Mime;
22
using System.Security.Cryptography;
3+
using System.Text;
34
using System.Text.Encodings.Web;
45
using FluentStorage;
56
using FluentStorage.Blobs;
67
using GZCTF.Models.Internal;
78
using GZCTF.Providers;
89
using GZCTF.Services.Cache;
10+
using Microsoft.AspNetCore.Localization;
911
using Microsoft.EntityFrameworkCore;
1012
using Microsoft.Extensions.Caching.Distributed;
1113
using Microsoft.Extensions.Options;
@@ -25,6 +27,15 @@ public static class ConfigurationExtension
2527
"font-src * 'self' data:; object-src 'none'; frame-src * https:; " +
2628
"connect-src 'self'; base-uri 'none';";
2729

30+
static readonly HashSet<string> SupportedCultures = Program.SupportedCultures
31+
.Select(c => c.ToLower()).ToHashSet();
32+
33+
static readonly HashSet<string> ShortSupportedCultures = SupportedCultures
34+
.Select(c => c.Split('-')[0]).ToHashSet();
35+
36+
const StringSplitOptions DefaultSplitOptions =
37+
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
38+
2839
public static void AddEntityConfiguration(this IConfigurationBuilder builder,
2940
Action<DbContextOptionsBuilder> optionsAction) =>
3041
builder.Add(new EntityConfigurationSource(optionsAction));
@@ -51,7 +62,8 @@ public static async Task UseIndexAsync(this WebApplication app)
5162
using var streamReader = new StreamReader(stream);
5263
var template = await streamReader.ReadToEndAsync();
5364

54-
#pragma warning disable RDG002 // Unable to resolve endpoint handler: https://github.com/dotnet/core/issues/8288
65+
#pragma warning disable RDG002
66+
// Unable to resolve endpoint handler: https://github.com/dotnet/core/issues/8288
5567
app.MapFallback(IndexHandler(template));
5668
#pragma warning restore RDG002
5769
}
@@ -107,6 +119,28 @@ static async Task<IResult> FaviconHandler(
107119
entityTag: EntityTagHeaderValue.Parse(eTag));
108120
}
109121

122+
static string ExtractLanguage(string? acceptLanguage)
123+
{
124+
if (string.IsNullOrWhiteSpace(acceptLanguage) || acceptLanguage.Length > 100)
125+
return "en-us";
126+
127+
foreach (var language in acceptLanguage.Split(',', DefaultSplitOptions))
128+
{
129+
var culture = language.Split(';', DefaultSplitOptions).First().ToLower();
130+
if (SupportedCultures.Contains(culture))
131+
return culture;
132+
133+
if (culture is "*")
134+
return "en-us";
135+
136+
var shortCulture = culture.Split('-', DefaultSplitOptions).First();
137+
if (ShortSupportedCultures.Contains(shortCulture))
138+
return shortCulture;
139+
}
140+
141+
return "en-us";
142+
}
143+
110144
static IndexHandlerDelegate IndexHandler(string template) => async (
111145
HttpContext context,
112146
IDistributedCache cache,
@@ -125,11 +159,16 @@ static IndexHandlerDelegate IndexHandler(string template) => async (
125159
await cache.SetStringAsync(CacheKey.Index, content, token);
126160
}
127161

128-
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(12));
162+
var builder = new StringBuilder(content);
163+
164+
var acceptLanguage = context.Request.Headers.AcceptLanguage;
165+
builder.Replace("%lang%", ExtractLanguage(acceptLanguage));
129166

167+
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(12));
130168
context.Response.Headers.ContentSecurityPolicy = string.Format(CspTemplate, nonce);
169+
builder.Replace("%nonce%", nonce);
131170

132-
return Results.Text(content.Replace("%nonce%", nonce), MediaTypeNames.Text.Html);
171+
return Results.Text(builder.ToString(), MediaTypeNames.Text.Html);
133172
};
134173

135174
delegate Task<IResult> IndexHandlerDelegate(

src/GZCTF/Program.cs

+24-25
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
#region Json
5656

5757
builder.Services.ConfigureHttpJsonOptions(options =>
58-
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default));
58+
{
59+
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
60+
options.SerializerOptions.Converters.Add(new DateTimeOffsetJsonConverter());
61+
});
5962

6063
#endregion Json
6164

@@ -64,24 +67,9 @@
6467
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources")
6568
.Configure<RequestLocalizationOptions>(options =>
6669
{
67-
string[] supportedCultures =
68-
[
69-
"en-US",
70-
"zh-CN",
71-
"zh-TW",
72-
"ja-JP",
73-
"id-ID",
74-
"ko-KR",
75-
"ru-RU",
76-
"de-DE",
77-
"fr-FR",
78-
"es-ES",
79-
"vi-VN"
80-
];
81-
8270
options
83-
.AddSupportedCultures(supportedCultures)
84-
.AddSupportedUICultures(supportedCultures);
71+
.AddSupportedCultures(GZCTF.Program.SupportedCultures)
72+
.AddSupportedUICultures(GZCTF.Program.SupportedCultures);
8573

8674
options.ApplyCurrentCultureToResponseHeaders = true;
8775
});
@@ -127,11 +115,6 @@
127115

128116
#region Configuration
129117

130-
builder.Services.ConfigureHttpJsonOptions(options =>
131-
{
132-
options.SerializerOptions.Converters.Add(new DateTimeOffsetJsonConverter());
133-
});
134-
135118
try
136119
{
137120
builder.Configuration.AddEntityConfiguration(options =>
@@ -166,7 +149,7 @@
166149
settings.Title = "GZCTF Server API";
167150
settings.Description = "GZCTF Server API Document";
168151
settings.UseControllerSummaryAsTagDescription = true;
169-
settings.SchemaSettings.TypeMappers.Add(new OpenAPIDateTimeOffsetToUIntMapper());
152+
settings.SchemaSettings.TypeMappers.Add(new OpenApiDateTimeOffsetToUIntMapper());
170153
settings.SchemaSettings.ReflectionService = new GenericsSystemTextJsonReflectionService();
171154
});
172155

@@ -318,6 +301,7 @@
318301
factory.Create(typeof(GZCTF.Resources.Program));
319302
}).AddJsonOptions(options =>
320303
{
304+
options.JsonSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
321305
options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetJsonConverter());
322306
});
323307

@@ -424,6 +408,21 @@ static Program()
424408
DefaultFaviconHash = Convert.ToHexStringLower(SHA256.HashData(DefaultFavicon));
425409
}
426410

411+
internal static readonly string[] SupportedCultures =
412+
[
413+
"en-US",
414+
"zh-CN",
415+
"zh-TW",
416+
"ja-JP",
417+
"id-ID",
418+
"ko-KR",
419+
"ru-RU",
420+
"de-DE",
421+
"fr-FR",
422+
"es-ES",
423+
"vi-VN"
424+
];
425+
427426
internal static IStringLocalizer<Program> StaticLocalizer { get; } =
428427
new CulturedLocalizer<Program>(CultureInfo.CurrentCulture);
429428

@@ -489,4 +488,4 @@ public static IActionResult InvalidModelStateHandler(ActionContext context)
489488
: localizer[nameof(Resources.Program.Model_ValidationFailed)])) { StatusCode = 400 };
490489
}
491490
}
492-
}
491+
}

src/GZCTF/Utils/JsonSerializeHelper.cs

-42
This file was deleted.

src/GZCTF/Utils/JsonSerializerContext.cs

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.Serialization;
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
23
using GZCTF.Models.Internal;
34
using GZCTF.Models.Request.Account;
45
using GZCTF.Models.Request.Admin;
@@ -7,7 +8,9 @@
78
using GZCTF.Models.Request.Info;
89
using GZCTF.Services.Container.Provider;
910
using Namotion.Reflection;
11+
using NJsonSchema;
1012
using NJsonSchema.Generation;
13+
using NJsonSchema.Generation.TypeMappers;
1114

1215
namespace GZCTF.Utils;
1316

@@ -60,6 +63,28 @@ namespace GZCTF.Utils;
6063
[JsonSerializable(typeof(TeamInfoModel[]))]
6164
internal sealed partial class AppJsonSerializerContext : JsonSerializerContext;
6265

66+
public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
67+
{
68+
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
69+
reader.TokenType == JsonTokenType.Number ?
70+
DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64()) : reader.GetDateTimeOffset();
71+
72+
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
73+
writer.WriteNumberValue(value.ToUnixTimeMilliseconds());
74+
}
75+
76+
public class OpenApiDateTimeOffsetToUIntMapper : ITypeMapper
77+
{
78+
public void GenerateSchema(JsonSchema schema, TypeMapperContext context)
79+
{
80+
schema.Type = JsonObjectType.Integer;
81+
schema.Format = JsonFormatStrings.ULong;
82+
}
83+
84+
public Type MappedType => typeof(DateTimeOffset);
85+
public bool UseReference => false;
86+
}
87+
6388
// wait for https://github.com/RicoSuter/NJsonSchema/issues/1741
6489
internal class GenericsSystemTextJsonReflectionService : SystemTextJsonReflectionService
6590
{

0 commit comments

Comments
 (0)