mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
fix: support Chinese cron log names (#3068)
This commit is contained in:
+2
-2
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -308,7 +308,7 @@
|
|||||||
"日志删除频率": "日志删除频率",
|
"日志删除频率": "日志删除频率",
|
||||||
"日志名称": "日志名称",
|
"日志名称": "日志名称",
|
||||||
"日志名称不能超过100个字符": "日志名称不能超过100个字符",
|
"日志名称不能超过100个字符": "日志名称不能超过100个字符",
|
||||||
"日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符",
|
"日志名称只能包含中文、字母、数字、下划线、连字符、点和路径分隔符": "日志名称只能包含中文、字母、数字、下划线、连字符、点和路径分隔符",
|
||||||
"日志文件": "日志文件",
|
"日志文件": "日志文件",
|
||||||
"日志管理": "日志管理",
|
"日志管理": "日志管理",
|
||||||
"时": "时",
|
"时": "时",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user