chanjs 2.7.3 → 2.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +224 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +174 -197
- package/package.json +11 -3
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +21 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +94 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/utils/file.js
CHANGED
|
@@ -1,284 +1,113 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 获取当前文件的目录路径
|
|
6
|
-
* @param {string} importMetaUrl - import.meta.url 的值
|
|
7
|
-
* @returns {string} 当前文件的目录路径
|
|
8
|
-
* @description
|
|
9
|
-
* 使用 import.meta.url 获取当前模块的 URL,然后提取目录路径
|
|
10
|
-
* 适用于 ES 模块中获取 __dirname 的替代方案
|
|
11
|
-
* @example
|
|
12
|
-
* const __dirname = dirname(import.meta.url);
|
|
13
|
-
* const configPath = path.join(__dirname, 'config.json');
|
|
14
|
-
*/
|
|
15
|
-
export function dirname(importMetaUrl) {
|
|
16
|
-
const url = new URL(importMetaUrl);
|
|
17
|
-
return path.dirname(url.pathname);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
1
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param {string} inputPath - 待检查的路径
|
|
24
|
-
* @returns {boolean} 是否安全
|
|
2
|
+
* 文件系统操作工具
|
|
3
|
+
* 提供安全的文件读写/删除/目录扫描功能(内置路径穿越防御)
|
|
25
4
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (path.isAbsolute(inputPath) && !inputPath.startsWith(process.cwd())) return false;
|
|
30
|
-
// 拒绝目录穿越
|
|
31
|
-
const normalized = path.normalize(inputPath);
|
|
32
|
-
if (normalized.includes('..')) return false;
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import logger from "./logger.js";
|
|
35
8
|
|
|
36
9
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @
|
|
40
|
-
* @param {string} fullPath - 已解析的绝对路径
|
|
41
|
-
* @returns {boolean} 是否在 cwd 内
|
|
10
|
+
* 路径安全校验:禁止越界项目根目录
|
|
11
|
+
* @param {string} inputPath
|
|
12
|
+
* @returns {string|null} 合法返回绝对路径,非法返回null
|
|
42
13
|
*/
|
|
43
|
-
function
|
|
14
|
+
export function safePath(inputPath) {
|
|
15
|
+
if (!inputPath || typeof inputPath !== "string") return null;
|
|
16
|
+
const fullPath = path.resolve(process.cwd(), inputPath);
|
|
44
17
|
const rel = path.relative(process.cwd(), fullPath);
|
|
45
|
-
|
|
18
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
19
|
+
return fullPath;
|
|
46
20
|
}
|
|
47
21
|
|
|
48
22
|
/**
|
|
49
|
-
*
|
|
50
|
-
* @param {string} filePath
|
|
51
|
-
* @returns {boolean}
|
|
52
|
-
* @description
|
|
53
|
-
* 删除指定路径的图片文件
|
|
54
|
-
* 路径是相对于项目根目录(process.cwd())的
|
|
55
|
-
*
|
|
56
|
-
* 安全改进:
|
|
57
|
-
* - 增加防御性路径校验,拒绝绝对路径和目录穿越
|
|
58
|
-
* - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
|
|
59
|
-
* - 业务层即使忘做校验也不会被利用删除任意文件
|
|
60
|
-
* @example
|
|
61
|
-
* const deleted = delImg('/uploads/avatar/123.jpg');
|
|
62
|
-
* if (deleted) {
|
|
63
|
-
* console.log('图片已删除');
|
|
64
|
-
* }
|
|
23
|
+
* 删除文件
|
|
24
|
+
* @param {string} filePath 相对于项目根目录的文件路径
|
|
25
|
+
* @returns {boolean}
|
|
65
26
|
*/
|
|
66
27
|
export function delImg(filePath) {
|
|
67
|
-
|
|
68
|
-
|
|
28
|
+
const fullPath = safePath(filePath);
|
|
29
|
+
if (!fullPath) {
|
|
30
|
+
logger.error("[delImg] 非法路径/路径越界:", filePath);
|
|
69
31
|
return false;
|
|
70
32
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
if (fs.existsSync(fullPath)) {
|
|
78
|
-
fs.unlinkSync(fullPath);
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 获取文件树结构
|
|
86
|
-
* @param {string} dirPath - 目录路径
|
|
87
|
-
* @param {boolean} [recursive=true] - 是否递归获取子目录
|
|
88
|
-
* @returns {Array} 文件树数组
|
|
89
|
-
* @description
|
|
90
|
-
* 递归读取目录下的所有文件和子目录,返回树形结构
|
|
91
|
-
* @example
|
|
92
|
-
* const tree = getFileTree('/path/to/directory');
|
|
93
|
-
* // 返回: [{ name: 'file.html', path: '/path/to/directory/file.html', type: 'file' }, ...]
|
|
94
|
-
*/
|
|
95
|
-
export function getFileTree(dirPath, recursive = true, basePath = null) {
|
|
96
|
-
// 路径安全校验:拒绝不安全路径直接返回空数组
|
|
97
|
-
if (!_isPathSafe(dirPath)) {
|
|
98
|
-
console.error('[getFileTree] 拒绝不安全路径:', dirPath);
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
if (!fs.existsSync(dirPath)) {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const stats = fs.statSync(dirPath);
|
|
106
|
-
if (!stats.isDirectory()) {
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 二次校验:解析后必须仍在 cwd 内
|
|
111
|
-
const fullPath = path.resolve(process.cwd(), dirPath);
|
|
112
|
-
if (!_isWithinCwd(fullPath)) {
|
|
113
|
-
console.error('[getFileTree] 路径越界:', dirPath);
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const items = [];
|
|
118
|
-
const files = fs.readdirSync(dirPath);
|
|
119
|
-
|
|
120
|
-
const base = basePath || process.cwd();
|
|
121
|
-
|
|
122
|
-
for (const file of files) {
|
|
123
|
-
const filePath = path.join(dirPath, file);
|
|
124
|
-
const fileStats = fs.statSync(filePath);
|
|
125
|
-
|
|
126
|
-
if (fileStats.isDirectory() && recursive) {
|
|
127
|
-
const children = getFileTree(filePath, recursive, basePath);
|
|
128
|
-
items.push({
|
|
129
|
-
name: file,
|
|
130
|
-
path: filePath,
|
|
131
|
-
type: 'directory',
|
|
132
|
-
children: children
|
|
133
|
-
});
|
|
134
|
-
} else if (fileStats.isFile()) {
|
|
135
|
-
const relativePath = path.relative(base, filePath).replace(/\\/g, '/');
|
|
136
|
-
items.push({
|
|
137
|
-
name: file,
|
|
138
|
-
path: filePath,
|
|
139
|
-
relativePath: relativePath.startsWith('public') ? '/' + relativePath.replace(/^\//, '') : filePath,
|
|
140
|
-
type: 'file',
|
|
141
|
-
size: fileStats.size
|
|
142
|
-
});
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(fullPath)) {
|
|
35
|
+
fs.unlinkSync(fullPath);
|
|
36
|
+
return true;
|
|
143
37
|
}
|
|
38
|
+
return false;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.error("[delImg] 删除文件异常", fullPath, err.message);
|
|
41
|
+
return false;
|
|
144
42
|
}
|
|
145
|
-
|
|
146
|
-
return items;
|
|
147
43
|
}
|
|
148
44
|
|
|
149
45
|
/**
|
|
150
|
-
*
|
|
151
|
-
* @param {string} filePath
|
|
152
|
-
* @returns {string}
|
|
153
|
-
* @
|
|
154
|
-
* 读取指定文件的内容
|
|
155
|
-
*
|
|
156
|
-
* 安全改进:
|
|
157
|
-
* - 增加防御性路径校验,拒绝绝对路径和目录穿越
|
|
158
|
-
* - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
|
|
159
|
-
* - 与 saveFileContent 保持一致的安全策略
|
|
160
|
-
* @example
|
|
161
|
-
* const content = readFileContent('/path/to/file.html');
|
|
46
|
+
* 读取文本文件内容
|
|
47
|
+
* @param {string} filePath
|
|
48
|
+
* @returns {string} utf8 文件文本内容
|
|
49
|
+
* @throws 非法路径或文件不存在时抛异常
|
|
162
50
|
*/
|
|
163
51
|
export function readFileContent(filePath) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (!_isWithinCwd(fullPath)) {
|
|
170
|
-
throw new Error(`[readFileContent] 路径越界: ${filePath}`);
|
|
171
|
-
}
|
|
172
|
-
if (!fs.existsSync(fullPath)) {
|
|
173
|
-
throw new Error('文件不存在');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const stats = fs.statSync(fullPath);
|
|
177
|
-
if (!stats.isFile()) {
|
|
178
|
-
throw new Error('路径不是文件');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return fs.readFileSync(fullPath, 'utf-8');
|
|
52
|
+
const fullPath = safePath(filePath);
|
|
53
|
+
if (!fullPath) throw new Error(`[readFileContent] 拒绝不安全/越界路径: ${filePath}`);
|
|
54
|
+
if (!fs.existsSync(fullPath)) throw new Error("文件不存在");
|
|
55
|
+
if (!fs.statSync(fullPath).isFile()) throw new Error("目标路径不是文件");
|
|
56
|
+
return fs.readFileSync(fullPath, "utf-8");
|
|
182
57
|
}
|
|
183
58
|
|
|
184
59
|
/**
|
|
185
|
-
*
|
|
186
|
-
* @param {string} filePath
|
|
187
|
-
* @param {string} content
|
|
188
|
-
* @
|
|
189
|
-
* @description
|
|
190
|
-
* 将内容保存到指定文件,如果目录不存在会自动创建
|
|
191
|
-
*
|
|
192
|
-
* 安全改进:
|
|
193
|
-
* - 增加防御性路径校验,拒绝绝对路径和目录穿越
|
|
194
|
-
* - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
|
|
195
|
-
* - 业务层即使忘做校验也不会被写入任意路径
|
|
196
|
-
* @example
|
|
197
|
-
* saveFileContent('/path/to/file.html', '<html>...</html>');
|
|
60
|
+
* 保存/覆写文件,不存在目录自动递归创建
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
* @param {string} content
|
|
63
|
+
* @throws 非法路径时抛异常
|
|
198
64
|
*/
|
|
199
65
|
export function saveFileContent(filePath, content) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
204
|
-
// 二次校验:解析后必须仍在 cwd 内
|
|
205
|
-
if (!_isWithinCwd(fullPath)) {
|
|
206
|
-
throw new Error(`[saveFileContent] 路径越界: ${filePath}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
66
|
+
const fullPath = safePath(filePath);
|
|
67
|
+
if (!fullPath) throw new Error(`[saveFileContent] 拒绝不安全/越界路径: ${filePath}`);
|
|
209
68
|
const dirPath = path.dirname(fullPath);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
69
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
70
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
216
71
|
}
|
|
217
72
|
|
|
218
73
|
/**
|
|
219
|
-
*
|
|
220
|
-
* @param {string}
|
|
221
|
-
* @
|
|
222
|
-
* @returns {boolean} 路径是否安全
|
|
223
|
-
* @description
|
|
224
|
-
* 检查目标路径是否在基础路径范围内,防止路径遍历攻击
|
|
225
|
-
* @example
|
|
226
|
-
* const safe = isPathSafe('/project/file.html', '/project');
|
|
74
|
+
* 获取指定目录下一级文件夹名称列表(过滤隐藏目录)
|
|
75
|
+
* @param {string} folderPath
|
|
76
|
+
* @returns {string[]}
|
|
227
77
|
*/
|
|
228
|
-
export function
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
78
|
+
export function getFolders(folderPath) {
|
|
79
|
+
const fullPath = safePath(folderPath);
|
|
80
|
+
if (!fullPath) {
|
|
81
|
+
logger.error("[getFolders] 非法路径/路径越界:", folderPath);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
86
|
+
return fs.readdirSync(fullPath)
|
|
87
|
+
.filter(name => !name.startsWith(".") && fs.statSync(path.join(fullPath, name)).isDirectory());
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logger.error("[getFolders] 读取目录异常", fullPath, err.message);
|
|
90
|
+
return [];
|
|
234
91
|
}
|
|
235
|
-
|
|
236
|
-
const relativePath = path.relative(normalizedBase, normalizedTarget);
|
|
237
|
-
return !relativePath.startsWith('..');
|
|
238
92
|
}
|
|
239
93
|
|
|
240
94
|
/**
|
|
241
|
-
*
|
|
242
|
-
* @param {string} folderPath
|
|
243
|
-
* @returns {
|
|
244
|
-
* @description
|
|
245
|
-
* 同步获取指定文件夹下的所有文件夹(不包含隐藏文件)
|
|
246
|
-
* @example
|
|
247
|
-
* const folders = getFolders('/path/to/directory');
|
|
248
|
-
* // 返回: ['default', 'test', ...]
|
|
95
|
+
* 同步获取目录下一级全部 .html 文件名称
|
|
96
|
+
* @param {string} folderPath
|
|
97
|
+
* @returns {string[]}
|
|
249
98
|
*/
|
|
250
|
-
export function
|
|
251
|
-
|
|
252
|
-
if (!
|
|
253
|
-
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
256
|
-
if (!fs.existsSync(folderPath)) {
|
|
99
|
+
export function getHtmlFilesSync(folderPath) {
|
|
100
|
+
const fullPath = safePath(folderPath);
|
|
101
|
+
if (!fullPath) {
|
|
102
|
+
logger.error("[getHtmlFilesSync] 非法路径/路径越界:", folderPath);
|
|
257
103
|
return [];
|
|
258
104
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
105
|
+
try {
|
|
106
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
107
|
+
return fs.readdirSync(fullPath)
|
|
108
|
+
.filter(name => fs.statSync(path.join(fullPath, name)).isFile() && path.extname(name).toLowerCase() === ".html");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.error("[getHtmlFilesSync] 读取目录异常", fullPath, err.message);
|
|
263
111
|
return [];
|
|
264
112
|
}
|
|
265
|
-
|
|
266
|
-
const items = fs.readdirSync(folderPath);
|
|
267
|
-
const folders = [];
|
|
268
|
-
|
|
269
|
-
for (const item of items) {
|
|
270
|
-
if (item.startsWith('.')) continue;
|
|
271
|
-
|
|
272
|
-
const itemPath = path.join(folderPath, item);
|
|
273
|
-
try {
|
|
274
|
-
const stat = fs.statSync(itemPath);
|
|
275
|
-
if (stat.isDirectory()) {
|
|
276
|
-
folders.push(item);
|
|
277
|
-
}
|
|
278
|
-
} catch (err) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return folders;
|
|
284
113
|
}
|
package/utils/filter.js
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param {Array<Object>} data - 要处理的对象数组
|
|
4
|
-
* @param {Array<string>} fields - 需要保留的字段名数组
|
|
5
|
-
* @returns {Array<Object>} 只包含指定字段的新对象数组
|
|
6
|
-
* @description
|
|
7
|
-
* 遍历数组中的每个对象,只保留指定的字段
|
|
8
|
-
* 返回一个新的数组,不修改原始数据
|
|
2
|
+
* 从对象数组批量筛选指定字段,返回全新数组与全新对象,不修改原始入参数据
|
|
9
3
|
* @example
|
|
10
|
-
*
|
|
11
|
-
* { id: 1, name: '张三'
|
|
12
|
-
*
|
|
13
|
-
* ]
|
|
14
|
-
*
|
|
15
|
-
* console.log(result);
|
|
16
|
-
* // [
|
|
17
|
-
* // { id: 1, name: '张三' },
|
|
18
|
-
* // { id: 2, name: '李四' }
|
|
19
|
-
* // ]
|
|
4
|
+
* filterFields([{ id: 1, name: '张三', age: 25 }], ['id', 'name'])
|
|
5
|
+
* // [{ id: 1, name: '张三' }]
|
|
6
|
+
* @param {Array<Object>} data 原始对象数组
|
|
7
|
+
* @param {string[]} fields 需要保留的字段名列表
|
|
8
|
+
* @returns {Array<Object>} 仅包含指定字段的新对象数组
|
|
20
9
|
*/
|
|
21
10
|
export function filterFields(data, fields) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
// 校验数组参数,非数组直接返回空数组
|
|
12
|
+
if (!Array.isArray(data)) return [];
|
|
13
|
+
// 字段列表非数组/空数组直接返回空对象数组
|
|
14
|
+
if (!Array.isArray(fields) || fields.length === 0) return data.map(() => ({}));
|
|
15
|
+
|
|
25
16
|
return data.map((item) => {
|
|
26
|
-
|
|
17
|
+
// 当前项不是普通对象,返回空对象规避取值报错
|
|
18
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const filtered = {};
|
|
27
22
|
for (const field of fields) {
|
|
28
|
-
//
|
|
23
|
+
// 跳过非字符串字段名,避免异常key
|
|
24
|
+
if (typeof field !== "string") continue;
|
|
25
|
+
// 使用原型方法,防止item重写hasOwnProperty造成原型污染漏洞
|
|
29
26
|
if (Object.prototype.hasOwnProperty.call(item, field)) {
|
|
30
|
-
|
|
27
|
+
filtered[field] = item[field];
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
|
-
return
|
|
30
|
+
return filtered;
|
|
34
31
|
});
|
|
35
|
-
}
|
|
32
|
+
}
|
package/utils/html.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* HTML 处理工具
|
|
3
|
+
* 提供 HTML 实体编解码、script标签深度转义、HTML文本提取图片URL功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 标准 HTML 实体编码(适配 art-template {{ }} 插值输出)
|
|
8
|
+
* @param {any} str 待转义文本
|
|
9
|
+
* @returns {string}
|
|
3
10
|
*/
|
|
4
11
|
export function htmlEncode(str) {
|
|
5
12
|
if (typeof str !== 'string') return '';
|
|
@@ -12,47 +19,56 @@ export function htmlEncode(str) {
|
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
22
|
+
* 深度危险标签二次转义(适配 art-template {{@ }} 原始输出防XSS)
|
|
23
|
+
* 双重转义 script/iframe/object/embed/form 标签
|
|
24
|
+
* @param {any} str 待处理HTML文本
|
|
25
|
+
* @returns {string}
|
|
17
26
|
*/
|
|
18
27
|
export const escapeScript = (str) => {
|
|
19
28
|
if (typeof str !== 'string') return '';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.replace(
|
|
23
|
-
|
|
29
|
+
// 一次性匹配所有危险开始/结束标签,统一替换为双重转义
|
|
30
|
+
return str.replace(/<\/?(script|iframe|object|embed|form)\b[^>]*>/gi, (match) => {
|
|
31
|
+
return match.replace(/</g, '&lt;').replace(/>/g, '&gt;');
|
|
32
|
+
});
|
|
24
33
|
};
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
|
-
* HTML
|
|
28
|
-
*
|
|
36
|
+
* HTML 实体解码
|
|
37
|
+
* 支持:命名实体(amp/lt/gt/quot/apos/nbsp)、十进制数字实体、十六进制Unicode实体
|
|
38
|
+
* @param {any} str 带实体编码的字符串
|
|
39
|
+
* @returns {string}
|
|
29
40
|
*/
|
|
41
|
+
const NAMED_ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ' ': ' ' };
|
|
42
|
+
|
|
30
43
|
export function htmlDecode(str) {
|
|
31
44
|
if (typeof str !== 'string') return '';
|
|
32
|
-
const entities = {
|
|
33
|
-
'&': '&',
|
|
34
|
-
'<': '<',
|
|
35
|
-
'>': '>',
|
|
36
|
-
'"': '"',
|
|
37
|
-
''': "'",
|
|
38
|
-
' ': ' ',
|
|
39
|
-
};
|
|
40
45
|
return str
|
|
41
|
-
.replace(/&(amp|lt|gt|quot|apos|nbsp);/g,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return String.fromCodePoint(parseInt(h, 16));
|
|
46
|
-
} catch {
|
|
47
|
-
return '';
|
|
48
|
-
}
|
|
46
|
+
.replace(/&(amp|lt|gt|quot|apos|nbsp);/g, match => NAMED_ENTITIES[match])
|
|
47
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hexStr) => {
|
|
48
|
+
try { return String.fromCodePoint(parseInt(hexStr, 16)); }
|
|
49
|
+
catch { return ''; }
|
|
49
50
|
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return String.fromCodePoint(parseInt(d, 10));
|
|
54
|
-
} catch {
|
|
55
|
-
return '';
|
|
56
|
-
}
|
|
51
|
+
.replace(/&#(\d+);/g, (_, decStr) => {
|
|
52
|
+
try { return String.fromCodePoint(parseInt(decStr, 10)); }
|
|
53
|
+
catch { return ''; }
|
|
57
54
|
});
|
|
58
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 从HTML字符串中批量提取全部img标签src图片地址
|
|
59
|
+
* @param {string} str 富文本/HTML源码
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
export function filterImgFromStr(str) {
|
|
63
|
+
if (typeof str !== 'string' || str.trim() === '') return [];
|
|
64
|
+
|
|
65
|
+
const imgReg = /<img\b[^>]*?(?:>|\/>)/gi;
|
|
66
|
+
const srcReg = /src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
67
|
+
|
|
68
|
+
const tagList = str.match(imgReg);
|
|
69
|
+
if (!tagList) return [];
|
|
70
|
+
|
|
71
|
+
return tagList
|
|
72
|
+
.map(tag => tag.match(srcReg)?.[1]?.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
package/utils/index.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 工具聚合层
|
|
3
|
+
*
|
|
4
|
+
* 业务侧使用方式:
|
|
5
|
+
* import { helper } from "chanjs";
|
|
6
|
+
* const { tree, getIp, formatDateFields } = helper;
|
|
7
|
+
*
|
|
8
|
+
* 也可直接从子模块导入(精确引用):
|
|
9
|
+
* import { getIp } from "chanjs/utils/ip.js";
|
|
3
10
|
*/
|
|
11
|
+
|
|
4
12
|
// 时间处理
|
|
5
|
-
export {
|
|
13
|
+
export { formatDateFields } from "./time.js";
|
|
6
14
|
|
|
7
15
|
// 文件操作
|
|
8
16
|
export {
|
|
9
|
-
dirname,
|
|
10
17
|
delImg,
|
|
11
|
-
getFileTree,
|
|
12
18
|
readFileContent,
|
|
13
19
|
saveFileContent,
|
|
14
|
-
isPathSafe,
|
|
15
20
|
getFolders,
|
|
21
|
+
getHtmlFilesSync,
|
|
16
22
|
} from "./file.js";
|
|
17
23
|
|
|
18
24
|
// HTML 处理
|
|
19
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
htmlEncode,
|
|
27
|
+
htmlDecode,
|
|
28
|
+
escapeScript,
|
|
29
|
+
filterImgFromStr,
|
|
30
|
+
} from "./html.js";
|
|
20
31
|
|
|
21
32
|
// IP 获取
|
|
22
33
|
export { getIp } from "./ip.js";
|
|
@@ -25,10 +36,13 @@ export { getIp } from "./ip.js";
|
|
|
25
36
|
export { request } from "./request.js";
|
|
26
37
|
|
|
27
38
|
// 数据解析
|
|
28
|
-
export {
|
|
39
|
+
export { arrToObj, getChildrenId } from "./data-parse.js";
|
|
29
40
|
|
|
30
41
|
// 树形结构
|
|
31
42
|
export { tree, treeById } from "./tree.js";
|
|
32
43
|
|
|
33
44
|
// 字段过滤
|
|
34
45
|
export { filterFields } from "./filter.js";
|
|
46
|
+
|
|
47
|
+
// 分页工具
|
|
48
|
+
export { pages } from "./pages.js";
|