From 5ba1188d50b3df5d5f23f843f23f03783606ba8f Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 11 Aug 2025 08:54:43 +0800 Subject: [PATCH] core: prohibit rtl characters in displayName --- framework/framework/package.json | 2 +- packages/hydrooj/src/handler/home.ts | 1 + packages/hydrooj/src/interface.ts | 1 + packages/hydrooj/src/model/setting.ts | 50 +++++++++++++++++---------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/framework/framework/package.json b/framework/framework/package.json index d81c4d7a9..d7837d80a 100644 --- a/framework/framework/package.json +++ b/framework/framework/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@types/fs-extra": "^11.0.4", - "@types/koa": "^2.15.0", + "@types/koa": "^3.0.0", "@types/koa-compress": "^4.0.6", "@types/koa__router": "^12.0.4", "@types/saslprep": "^1.0.3", diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index 268356823..e6817d6a4 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -366,6 +366,7 @@ function set(s: Setting, key: string, value: any) { if (s.family === 'setting_storage') return undefined; if (s.flag & setting.FLAG_DISABLED) return undefined; if ((s.flag & setting.FLAG_SECRET) && !value) return undefined; + if (s.validation && !s.validation(value)) throw new ValidationError(key); if (s.type === 'boolean') { if (value === 'on') return true; return false; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 05f4136db..913f36591 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -54,6 +54,7 @@ export interface Setting { name: string; desc: string; flag: number; + validation?: (val: any) => boolean; } export interface Authenticator { diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 3f0669038..382854ae3 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import yaml from 'js-yaml'; import { Dictionary } from 'lodash'; import moment from 'moment-timezone'; +import saslPrep from 'saslprep'; import Schema from 'schemastery'; import { LangConfig, parseLang } from '@hydrooj/common'; import { findFileSync, randomstring, retry } from '@hydrooj/utils'; @@ -47,6 +48,7 @@ export type SettingType = 'text' | 'yaml' | 'number' | 'float' | 'markdown' | 'p export const Setting = ( family: string, key: string, value: any = null, type: SettingType = 'text', name = '', desc = '', flag = 0, + validation?: (val: any) => boolean, ): _Setting => { let subType = ''; if (type === 'yaml' && typeof value !== 'string') { @@ -64,6 +66,7 @@ export const Setting = ( subType, type: typeof type === 'object' ? 'select' : type, range: typeof type === 'object' ? type : null, + validation, }; }; @@ -82,13 +85,16 @@ function schemaToSettings(schema: Schema) { if (s.dict) throw new Error('Dict is not supported here'); let flag = (s.meta?.hidden ? FLAG_HIDDEN : 0) | (s.meta?.disabled ? FLAG_DISABLED : 0); - const type = s.type === 'number' ? 'number' - : s.type === 'boolean' ? 'boolean' - : s.meta?.role === 'markdown' ? 'markdown' - : s.meta?.role === 'textarea' ? 'textarea' : 'text'; + const actualType = s.type === 'transform' ? s.inner.type : s.type; + const actualList = s.type === 'transform' ? s.inner.list : s.list; + const type = actualType === 'any' ? 'json' + : actualType === 'number' ? 'number' + : actualType === 'boolean' ? 'boolean' + : s.meta?.role === 'markdown' ? 'markdown' + : s.meta?.role === 'textarea' ? 'textarea' : 'text'; if (s.meta?.role === 'password') flag |= FLAG_SECRET; const options = {}; - for (const item of s.list || []) { + for (const item of actualList || []) { if (item.type !== 'const') throw new Error('List item must be a constant'); options[item.value] = item.meta?.description || item.value; } @@ -100,8 +106,16 @@ function schemaToSettings(schema: Schema) { desc: s.meta?.description, flag, subType: '', - type: s.list ? 'select' : type, - range: s.list ? options : null, + type: actualList ? 'select' : type, + range: actualList ? options : null, + validation: (v) => { + try { + (s as any)(v); + return true; + } catch (e) { + return false; + } + }, } as _Setting; }; if (!schema.dict) return []; @@ -263,18 +277,16 @@ DomainSetting( Setting('setting_storage', 'host', '', 'text', 'Custom host', null, FLAG_HIDDEN | FLAG_DISABLED), ); -DomainUserSetting( - Setting('setting_info', 'displayName', '', 'text', 'Display Name'), - Setting('setting_storage', 'nAccept', 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'nSubmit', 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'nLike', 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'rp', 0, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'rpInfo', {}, 'json', 'RP Detail', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'rpdelta', 0, 'number', 'RP.delta', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'rank', 0, 'number', 'Rank', null, FLAG_DISABLED | FLAG_HIDDEN), - Setting('setting_storage', 'level', 0, 'number', 'level', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'join', 0, 'number', 'join', null, FLAG_HIDDEN | FLAG_DISABLED), -); +DomainUserSetting(Schema.object({ + displayName: Schema.transform(String, (input) => saslPrep(input)).default('').description('Display Name').extra('family', 'setting_info'), + + rpInfo: Schema.any().extra('family', 'setting_storage').disabled().hidden(), + + ...Object.fromEntries(['nAccept', 'nSubmit', 'nLike', 'rp', 'rpdelta', 'rank', 'level', 'join'].map((i) => ([ + i, Schema.number().default(0).extra('family', 'setting_storage').disabled().hidden(), + ]))), + +})); const ignoreUA = [ 'bingbot',