fix: support Chinese cron log names (#3068)

This commit is contained in:
whyour
2026-09-12 03:03:25 +08:00
parent 4df52094f2
commit d62d8f3025
6 changed files with 120 additions and 8 deletions
+2 -2
View File
@@ -94,8 +94,8 @@ const messages: Record<string, Record<string, string>> = {
'日志设置为忽略': 'Log set to ignore', '日志设置为忽略': 'Log set to ignore',
'定时规则不能为空': 'Schedule rule cannot be empty', '定时规则不能为空': 'Schedule rule cannot be empty',
'无效的定时规则': 'Invalid schedule rule', '无效的定时规则': 'Invalid schedule rule',
'日志名称只能包含字母、数字、下划线连字符': '日志名称只能包含中文、字母、数字、下划线连字符、点和路径分隔符':
'Log name can only contain letters, numbers, underscores, and hyphens', 'Log name can only contain Chinese characters, letters, numbers, underscores, hyphens, dots, and path separators',
'日志名称不能超过100个字符': 'Log name cannot exceed 100 characters', '日志名称不能超过100个字符': 'Log name cannot exceed 100 characters',
'错误的用户名密码,请重试': 'Incorrect username or password, please try again', '错误的用户名密码,请重试': 'Incorrect username or password, please try again',
'该 IP 已被列入黑名单': 'This IP address has been blocked', '该 IP 已被列入黑名单': 'This IP address has been blocked',
+2 -2
View File
@@ -75,7 +75,7 @@ export const commonCronSchema = {
} }
if ( if (
!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test( !/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w\p{Script=Han}.-]+\/)*[\w\p{Script=Han}.-]+\/?$/u.test(
value, value,
) )
) { ) {
@@ -87,7 +87,7 @@ export const commonCronSchema = {
return value; return value;
}) })
.messages({ .messages({
'string.pattern.base': '日志名称只能包含字母、数字、下划线连字符', 'string.pattern.base': '日志名称只能包含中文、字母、数字、下划线连字符、点和路径分隔符',
'string.max': '日志名称不能超过100个字符', 'string.max': '日志名称不能超过100个字符',
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null', 'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
}), }),
+1 -1
View File
@@ -309,7 +309,7 @@
"日志删除频率": "Log Deletion Frequency", "日志删除频率": "Log Deletion Frequency",
"日志名称": "Log Name", "日志名称": "Log Name",
"日志名称不能超过100个字符": "Log name cannot exceed 100 characters", "日志名称不能超过100个字符": "Log name cannot exceed 100 characters",
"日志名称只能包含字母、数字、下划线连字符": "Log name can only contain letters, numbers, underscores and hyphens", "日志名称只能包含中文、字母、数字、下划线连字符、点和路径分隔符": "Log name can only contain Chinese characters, letters, numbers, underscores, hyphens, dots, and path separators",
"日志文件": "Log files", "日志文件": "Log files",
"日志管理": "Log Management", "日志管理": "Log Management",
"时": "hour(s)", "时": "hour(s)",
+1 -1
View File
@@ -308,7 +308,7 @@
"日志删除频率": "日志删除频率", "日志删除频率": "日志删除频率",
"日志名称": "日志名称", "日志名称": "日志名称",
"日志名称不能超过100个字符": "日志名称不能超过100个字符", "日志名称不能超过100个字符": "日志名称不能超过100个字符",
"日志名称只能包含字母、数字、下划线连字符": "日志名称只能包含字母、数字、下划线连字符", "日志名称只能包含中文、字母、数字、下划线连字符、点和路径分隔符": "日志名称只能包含中文、字母、数字、下划线连字符、点和路径分隔符",
"日志文件": "日志文件", "日志文件": "日志文件",
"日志管理": "日志管理", "日志管理": "日志管理",
"时": "时", "时": "时",
+4 -2
View File
@@ -212,12 +212,14 @@ const CronModal = ({
return Promise.reject(intl.get('日志名称不能超过100个字符')); return Promise.reject(intl.get('日志名称不能超过100个字符'));
} }
if ( if (
!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test( !/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w\p{Script=Han}.-]+\/)*[\w\p{Script=Han}.-]+\/?$/u.test(
value, value,
) )
) { ) {
return Promise.reject( return Promise.reject(
intl.get('日志名称只能包含字母、数字、下划线和连字符'), intl.get(
'日志名称只能包含中文、字母、数字、下划线、连字符、点和路径分隔符',
),
); );
} }
return Promise.resolve(); return Promise.resolve();
+110
View File
@@ -0,0 +1,110 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const ts = require('typescript');
const loadModule = require('../helpers/load-security-module.cjs');
const { commonCronSchema } = loadModule(
path.join(__dirname, '../../back/validation/schedule.ts'),
{ '../config': { logPath: '/ql/data/log/' } },
);
// Execute the actual form validator without mounting the entire task modal.
const source = ts.createSourceFile(
'modal.tsx',
fs.readFileSync(
path.join(__dirname, '../../src/pages/crontab/modal.tsx'),
'utf8',
),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
let validateForm;
function visit(node) {
if (
ts.isJsxOpeningElement(node) &&
node.attributes.properties.some(
(attr) =>
ts.isJsxAttribute(attr) &&
attr.name.text === 'name' &&
attr.initializer?.text === 'log_name',
)
) {
const rules = node.attributes.properties.find(
(attr) => ts.isJsxAttribute(attr) && attr.name.text === 'rules',
);
const validator = rules.initializer.expression.elements[0].properties.find(
(property) => property.name.text === 'validator',
).initializer;
const { outputText } = ts.transpileModule(
`const validate = ${validator.getText(source)};`,
{ compilerOptions: { target: ts.ScriptTarget.ES2017 } },
);
validateForm = new Function('intl', `${outputText}\nreturn validate;`)({
get: (key) => key,
});
}
ts.forEachChild(node, visit);
}
visit(source);
assert.equal(typeof validateForm, 'function');
test('form and API accept Chinese log names and existing supported names', async () => {
for (const value of [
'',
null,
undefined,
'测试',
'任务_測試-2026.log',
'分组/每日签到/',
'𠮷/扩展汉字',
'legacy_name-123.log',
'legacy/path/',
'/ql/data/log/中文日志',
'/dev/null',
'中'.repeat(100),
]) {
await validateForm(undefined, value);
assert.equal(
commonCronSchema.log_name.validate(value).error,
undefined,
value,
);
}
});
test('form and API still reject unsafe relative names and excessive length', async () => {
for (const value of [
'.',
'..',
'../测试',
'测试/../日志',
'测试/./日志',
'测试//日志',
'测试\\日志',
'测试 日志',
'测试;echo',
'测试$(id)',
'测试`id`',
'测试\n日志',
'测试\0日志',
'测试😀',
'中'.repeat(101),
]) {
await assert.rejects(validateForm(undefined, value), undefined, value);
assert.ok(commonCronSchema.log_name.validate(value).error, value);
}
});
test('API restricts absolute log paths to the configured directory or /dev/null', () => {
for (const value of [
'/tmp/测试',
'/ql/data/log/../测试',
'/ql/data/log-other/测试',
]) {
const { error } = commonCronSchema.log_name.validate(value);
assert.equal(error.details[0].type, 'string.unsafePath', value);
}
});