- 新增地址选择插件,支持地图位置选择并返回地址和经纬度 - 新增微信订阅消息发送功能,支持审核结果、提现通知等场景 - 新增订单支付状态查询、提现收款人姓名保存等接口 - 新增服务区域管理,支持按省市区划分并同步到信息发布 - 新增资金流水和营收报表导出功能 - 新增可用优惠券查询、订单退款拆分处理等功能 - 优化后台角标显示、首页数据统计、UI配置下发等 - 修复订单支付并发问题、地区管理逻辑等
221 行
9.4 KiB
JavaScript
可执行文件
221 行
9.4 KiB
JavaScript
可执行文件
'use strict';
|
|
|
|
// Run with: node tests/attachment-formatters.test.js
|
|
// The real AMD modules run in a VM; no network or script payload is executed.
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const {test} = require('node:test');
|
|
|
|
function loadBackendFormatters() {
|
|
let tableOptions;
|
|
const chain = new Proxy(function () {}, {
|
|
get: (target, property) => property === 'bootstrapTable' ? options => {
|
|
tableOptions = options;
|
|
return chain;
|
|
} : chain,
|
|
apply: () => chain
|
|
});
|
|
const jquery = Object.assign(function () { return chain; }, {
|
|
fn: {extend() {}, bootstrapTable: {defaults: {extend: {}}}},
|
|
ajaxSetup() {}, extend: Object.assign
|
|
});
|
|
const layer = {config() {}};
|
|
const context = vm.createContext({
|
|
window: {}, document: {}, console, Config: {categoryList: {}},
|
|
define(dependencies, factory) {
|
|
context.module = factory(jquery, undefined, {}, layer, {});
|
|
}
|
|
});
|
|
const jsRoot = path.resolve(process.argv[2] || path.join(__dirname, '../public/assets/js'));
|
|
vm.runInContext(fs.readFileSync(path.join(jsRoot, 'fast.js'), 'utf8'), context);
|
|
context.Fast = context.module;
|
|
context.__ = context.module.lang;
|
|
context.Fast.api.fixurl = value => '/admin/' + value;
|
|
context.define = (dependencies, factory) => {
|
|
context.module = factory(jquery, undefined, {}, {}, {
|
|
api: {init() {}, bindevent() {}, events: {operate: {}}, formatter: {search: value => value}}
|
|
}, {serial: () => ''});
|
|
};
|
|
vm.runInContext(fs.readFileSync(path.join(jsRoot, 'backend/general/attachment.js'), 'utf8'), context);
|
|
context.module.index();
|
|
return {
|
|
formatters: context.module.api ? context.module.api.formatter : {},
|
|
columns: tableOptions.columns[0]
|
|
};
|
|
}
|
|
|
|
function loadFrontendFormatters() {
|
|
let tableOptions;
|
|
const chain = new Proxy(function () {}, {
|
|
get: (target, property) => property === 'bootstrapTable' ? options => {
|
|
tableOptions = options;
|
|
return chain;
|
|
} : chain,
|
|
apply: () => chain
|
|
});
|
|
const jquery = Object.assign(function () { return chain; }, {
|
|
fn: {extend() {}, bootstrapTable: {defaults: {extend: {}}}},
|
|
ajaxSetup() {}, extend: Object.assign, each() {}
|
|
});
|
|
const table = {
|
|
api: {
|
|
init() {}, bindevent() {},
|
|
formatter: {search: value => value, datetime: value => value}
|
|
}
|
|
};
|
|
const context = vm.createContext({
|
|
window: {location: {href: 'http://example.test/index/user/attachment'}}, document: {}, console,
|
|
define(dependencies, factory) {
|
|
context.module = factory(jquery, undefined, {}, {config() {}}, {});
|
|
}
|
|
});
|
|
const jsRoot = path.resolve(process.argv[2] || path.join(__dirname, '../public/assets/js'));
|
|
vm.runInContext(fs.readFileSync(path.join(jsRoot, 'fast.js'), 'utf8'), context);
|
|
context.Fast = context.module;
|
|
context.__ = context.module.lang;
|
|
context.Fast.api.fixurl = value => '/index/' + value;
|
|
context.require = (dependencies, callback) => {
|
|
callback(dependencies[0] === 'upload' ? {api: {upload() {}}} : table);
|
|
};
|
|
context.define = (dependencies, factory) => {
|
|
context.module = factory(jquery, undefined, {}, {api: {bindevent() {}}}, {});
|
|
};
|
|
vm.runInContext(fs.readFileSync(path.join(jsRoot, 'frontend/user.js'), 'utf8'), context);
|
|
context.module.attachment();
|
|
return {
|
|
formatters: context.module.api ? context.module.api.formatter : {},
|
|
columns: tableOptions.columns[0]
|
|
};
|
|
}
|
|
|
|
const {formatters, columns} = loadBackendFormatters();
|
|
const {formatters: frontendFormatters, columns: frontendColumns} = loadFrontendFormatters();
|
|
function attachment(overrides) {
|
|
return Object.assign({
|
|
fullurl: '/uploads/example.png', url: '/uploads/example.png',
|
|
thumb_style: '', mimetype: 'image/png', imagetype: 'png'
|
|
}, overrides);
|
|
}
|
|
|
|
test('image preview encodes URL attributes and thumbnail styles', () => {
|
|
const row = attachment({
|
|
fullurl: '/uploads/picture.png"><span data-audit="url">',
|
|
thumb_style: '"><span data-audit="style">'
|
|
});
|
|
const html = formatters.thumb('', row, 0);
|
|
assert.equal((html.match(/<a\b/g) || []).length, 1);
|
|
assert.equal((html.match(/<img\b/g) || []).length, 1);
|
|
assert.ok(!html.includes('<span'));
|
|
assert.ok(html.includes('"><span'));
|
|
});
|
|
|
|
test('attachment URL label is text and cannot create HTML', () => {
|
|
const row = attachment({url: '<span data-audit="label">A&B</span>'});
|
|
const html = formatters.url('', row, 0);
|
|
assert.ok(!html.includes('<span'));
|
|
assert.ok(html.includes('<span'));
|
|
assert.ok(html.includes('A&B'));
|
|
});
|
|
|
|
test('icon suffix is encoded as one query parameter', () => {
|
|
const row = attachment({mimetype: 'application/pdf', imagetype: 'pdf" data-audit="icon&other=1'});
|
|
const html = formatters.thumb('', row, 0);
|
|
assert.ok(!html.includes('data-audit="icon'));
|
|
assert.ok(html.includes('suffix=pdf%22%20data-audit%3D%22icon%26other%3D1'));
|
|
});
|
|
|
|
test('active URL schemes and control-character variants cannot be links', () => {
|
|
for (const fullurl of ['javascript:void(0)', 'JaVaScRiPt:void(0)', '\tjavascript:void(0)',
|
|
'java\nscript:void(0)', 'data:text/html,example', 'vbscript:example']) {
|
|
const row = attachment({fullurl});
|
|
for (const name of ['url', 'thumb']) {
|
|
const html = formatters[name]('', row, 0);
|
|
assert.ok(!html.includes(fullurl), name + ' rejects ' + JSON.stringify(fullurl));
|
|
assert.ok(html.includes('href=""'));
|
|
}
|
|
}
|
|
});
|
|
|
|
test('local and cloud attachment URLs keep their meaning', () => {
|
|
for (const fullurl of ['/uploads/image.png', 'uploads/image.png',
|
|
'https://cdn.example.test/image.png?size=100&mode=fit', '//cdn.example.test/image.png']) {
|
|
const html = formatters.thumb('', attachment({fullurl}), 0);
|
|
assert.ok(html.includes('href="' + fullurl.replace(/&/g, '&') + '"'));
|
|
}
|
|
});
|
|
|
|
test('editable metadata is escaped before display or search formatting', () => {
|
|
const value = '<span data-audit="metadata">A&B</span>';
|
|
for (const name of ['text', 'search', 'filename', 'mimetype']) {
|
|
const html = formatters[name].call({field: 'filename'}, value, attachment(), 0);
|
|
assert.ok(!html.includes('<span'));
|
|
assert.ok(html.includes('<span'));
|
|
assert.ok(html.includes('A&B'));
|
|
}
|
|
assert.equal(formatters.text(640), '640');
|
|
assert.equal(formatters.text(null), '');
|
|
for (const field of ['imagewidth', 'imageheight', 'imagetype', 'storage', 'filename', 'mimetype']) {
|
|
const column = columns.find(column => column.field === field);
|
|
assert.equal(typeof column.formatter, 'function', field + ' has a safe formatter');
|
|
assert.ok(!column.formatter.call(column, value, attachment(), 0).includes('<span'));
|
|
}
|
|
});
|
|
|
|
test('legacy encoded filenames display once while remaining HTML-safe', () => {
|
|
const row = attachment({filename: 'audit&A.png'});
|
|
const filename = formatters.filename.call({field: 'filename'}, 'audit&amp;A.png', row, 0);
|
|
assert.ok(filename.includes('audit&A.png'));
|
|
assert.ok(!filename.includes('audit&amp;A.png'));
|
|
|
|
const encodedMarkup = formatters.filename.call(
|
|
{field: 'filename'},
|
|
'<img src=x onerror=alert(1)>',
|
|
attachment({filename: '<img src=x onerror=alert(1)>'}),
|
|
0
|
|
);
|
|
assert.ok(encodedMarkup.includes('<img'));
|
|
assert.ok(!encodedMarkup.includes('<img'));
|
|
});
|
|
|
|
test('frontend user attachment preview encodes the same stored values', () => {
|
|
const row = attachment({
|
|
fullurl: '/missing.png"><span data-audit="frontend">',
|
|
thumb_style: '" onerror="alert(1)'
|
|
});
|
|
const html = frontendFormatters.thumb('', row, 0);
|
|
assert.equal((html.match(/<a\b/g) || []).length, 1);
|
|
assert.equal((html.match(/<img\b/g) || []).length, 1);
|
|
assert.ok(!html.includes('<span'));
|
|
assert.ok(!html.includes('onerror="'));
|
|
assert.ok(html.includes('"><span'));
|
|
});
|
|
|
|
test('frontend user attachment links reject active schemes', () => {
|
|
for (const fullurl of ['javascript:void(0)', 'data:text/html,example', 'java\nscript:void(0)']) {
|
|
const html = frontendFormatters.thumb('', attachment({fullurl}), 0);
|
|
assert.ok(html.includes('href=""'));
|
|
assert.ok(!html.includes(fullurl));
|
|
}
|
|
});
|
|
|
|
test('frontend metadata is escaped without double-encoding upload filenames', () => {
|
|
const filename = frontendFormatters.filename.call(
|
|
{field: 'filename'}, 'audit&amp;A.png', attachment({filename: 'audit&A.png'}), 0
|
|
);
|
|
assert.ok(filename.includes('audit&A.png'));
|
|
assert.ok(!filename.includes('audit&amp;A.png'));
|
|
|
|
const value = '<span data-audit="frontend-metadata">A&B</span>';
|
|
const mimetype = frontendFormatters.mimetype.call(
|
|
{field: 'mimetype'}, value, attachment({mimetype: value}), 0
|
|
);
|
|
assert.ok(!mimetype.includes('<span'));
|
|
assert.ok(mimetype.includes('<span'));
|
|
for (const field of ['imagewidth', 'imageheight', 'filename', 'mimetype']) {
|
|
const column = frontendColumns.find(column => column.field === field);
|
|
assert.equal(typeof column.formatter, 'function', field + ' has a frontend formatter');
|
|
}
|
|
});
|