/** 工单附件展示适配;文件名、存储名与可访问 URL 分开,不把存储键直接当链接。 */ // 允许 HTTP(S) 和显式相对路径,拒绝控制字符、反斜杠及脚本/data 等协议。 function e(e) { if ( !/^(?:https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(e) || // eslint-disable-next-line no-control-regex -- 安全边界:拒绝附件 URL 内的控制字符。 /[\u0000-\u001f\u007f\\]/.test(e) ) return !1; try { let t = new URL(e, `https://attachment.invalid/`); return t.protocol === `http:` || t.protocol === `https:`; } catch { return !1; } } // 仅在地址合法且文件名为支持的位图扩展名时启用图片预览,SVG 不走内嵌预览。 function t(t) { return ( !!t.href && e(t.href) && /\.(?:avif|bmp|gif|ico|jpe?g|png|webp)$/i.test(t.name) ); } // 历史存储名使用 ^ 分隔前缀,展示时取末段;URI 解码失败仍保留可读的原名。 function n(e, t) { let n = e .split(`/`) .at(-1) ?.split(/\^|%5e/i) .at(-1) || t; try { n = decodeURIComponent(n); } catch {} return n.split(`^`).at(-1) || t; } /** 兼容字符串及对象附件;无地址仍保留名称/存储名和明确占位,不伪造下载链接。 */ function r(t) { return Array.isArray(t) ? t.map((t, r) => { let i = `附件 ${r + 1}`, a = t && typeof t == `object` && !Array.isArray(t) ? t : null; if ( typeof t != `string` && (!a || ![`url`, `name`, `save_name`].some((e) => typeof a[e] == `string`)) ) return { name: `${i}(格式暂不支持)`, href: null, }; let o = typeof t == `string` ? t : a?.url, s = typeof o == `string` ? o.trim() : ``, c = typeof a?.name == `string` ? a.name.trim() : ``, l = typeof a?.save_name == `string` ? a.save_name.trim() : ``, u = c || (l ? n(l, i) : ``), d = l || // eslint-disable-next-line no-control-regex -- 安全边界:拒绝附件名中的控制字符。 (typeof o == `string` && !e(o) && !/[\\/:\u0000-\u001f]/.test(o) ? o : ``), f = { name: u || (d ? n(d, i) : `${i}(地址不可用)`), href: null, ...(d ? { storageName: d, } : {}), }; if (!e(s)) return f; try { let e = new URL(s, `https://attachment.invalid/`); return e.protocol !== `http:` && e.protocol !== `https:` ? f : { name: u || n(e.pathname, i), href: s, ...(d ? { storageName: d, } : {}), }; } catch { return f; } }) : []; } export { r as n, t };