迁移聊天消息安全工具
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 模块引用说明:
|
* 模块引用说明:
|
||||||
* - html.js:提供聊天内容通用 HTML 转义工具。
|
* - html.js:提供聊天内容通用 HTML 转义和安全链接规整工具。
|
||||||
* - appointment-announcement.js:处理任命/撤销公告的大卡片和系统消息。
|
* - appointment-announcement.js:处理任命/撤销公告的大卡片和系统消息。
|
||||||
* - banner.js:提供 window.chatBanner 居中大卡片通知。
|
* - banner.js:提供 window.chatBanner 居中大卡片通知。
|
||||||
* - chat-bot.js:处理 AI 小班长发送消息和清空上下文。
|
* - chat-bot.js:处理 AI 小班长发送消息和清空上下文。
|
||||||
@@ -54,10 +54,11 @@
|
|||||||
* - reward-modal.js:处理职务奖励金币弹窗入口。
|
* - reward-modal.js:处理职务奖励金币弹窗入口。
|
||||||
* - red-packet-panel.js:处理礼包红包发包、抢包、倒计时和广播监听。
|
* - red-packet-panel.js:处理礼包红包发包、抢包、倒计时和广播监听。
|
||||||
* - message-queue.js:提供聊天消息分批渲染队列。
|
* - message-queue.js:提供聊天消息分批渲染队列。
|
||||||
|
* - message-utils.js:提供图片消息过期等消息渲染辅助判断。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。
|
// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。
|
||||||
export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
export { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js";
|
||||||
export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
||||||
export { bindChatBanner } from "./chat-room/banner.js";
|
export { bindChatBanner } from "./chat-room/banner.js";
|
||||||
export { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
|
export { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
|
||||||
@@ -241,8 +242,9 @@ export {
|
|||||||
updateRedPacketClaimsUI,
|
updateRedPacketClaimsUI,
|
||||||
} from "./chat-room/red-packet-panel.js";
|
} from "./chat-room/red-packet-panel.js";
|
||||||
export { createMessageQueue } from "./chat-room/message-queue.js";
|
export { createMessageQueue } from "./chat-room/message-queue.js";
|
||||||
|
export { isExpiredChatImageMessage } from "./chat-room/message-utils.js";
|
||||||
|
|
||||||
import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
import { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js";
|
||||||
import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
||||||
import { bindChatBanner } from "./chat-room/banner.js";
|
import { bindChatBanner } from "./chat-room/banner.js";
|
||||||
import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
|
import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
|
||||||
@@ -426,12 +428,14 @@ import {
|
|||||||
updateRedPacketClaimsUI,
|
updateRedPacketClaimsUI,
|
||||||
} from "./chat-room/red-packet-panel.js";
|
} from "./chat-room/red-packet-panel.js";
|
||||||
import { createMessageQueue } from "./chat-room/message-queue.js";
|
import { createMessageQueue } from "./chat-room/message-queue.js";
|
||||||
|
import { isExpiredChatImageMessage } from "./chat-room/message-utils.js";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// 保留聚合入口,给新迁移模块、测试和仍在 Blade 内的存量脚本统一读取工具。
|
// 保留聚合入口,给新迁移模块、测试和仍在 Blade 内的存量脚本统一读取工具。
|
||||||
window.ChatRoomTools = {
|
window.ChatRoomTools = {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
escapeHtmlWithLineBreaks,
|
escapeHtmlWithLineBreaks,
|
||||||
|
normalizeSafeChatUrl,
|
||||||
bindAppointmentAnnouncementControls,
|
bindAppointmentAnnouncementControls,
|
||||||
showAppointmentBanner,
|
showAppointmentBanner,
|
||||||
bindChatBanner,
|
bindChatBanner,
|
||||||
@@ -643,10 +647,14 @@ if (typeof window !== "undefined") {
|
|||||||
showRedPacketModal,
|
showRedPacketModal,
|
||||||
updateRedPacketClaimsUI,
|
updateRedPacketClaimsUI,
|
||||||
createMessageQueue,
|
createMessageQueue,
|
||||||
|
isExpiredChatImageMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 直接挂载只服务暂未迁移的 Blade 调用点;新代码优先通过模块导入或 ChatRoomTools 复用。
|
// 直接挂载只服务暂未迁移的 Blade 调用点;新代码优先通过模块导入或 ChatRoomTools 复用。
|
||||||
window.closeChatImageLightbox = closeChatImageLightbox;
|
window.closeChatImageLightbox = closeChatImageLightbox;
|
||||||
|
window.escapeHtml = escapeHtml;
|
||||||
|
window.isExpiredChatImageMessage = isExpiredChatImageMessage;
|
||||||
|
window.normalizeSafeChatUrl = normalizeSafeChatUrl;
|
||||||
window.openChatImageLightbox = openChatImageLightbox;
|
window.openChatImageLightbox = openChatImageLightbox;
|
||||||
window.closeFriendPanel = closeFriendPanel;
|
window.closeFriendPanel = closeFriendPanel;
|
||||||
window.friendSearch = friendSearch;
|
window.friendSearch = friendSearch;
|
||||||
|
|||||||
@@ -27,3 +27,28 @@ export function escapeHtml(value) {
|
|||||||
export function escapeHtmlWithLineBreaks(value) {
|
export function escapeHtmlWithLineBreaks(value) {
|
||||||
return escapeHtml(value).replace(/\n/g, "<br>");
|
return escapeHtml(value).replace(/\n/g, "<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规整广播携带的链接,只允许当前站点的 http(s) 地址进入 innerHTML。
|
||||||
|
*
|
||||||
|
* @param {string|null|undefined} url 原始链接
|
||||||
|
* @param {string} fallback 回退链接
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function normalizeSafeChatUrl(url, fallback) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url || fallback, window.location.origin);
|
||||||
|
|
||||||
|
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl.origin !== window.location.origin) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedUrl.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 聊天消息工具函数,承接主消息渲染脚本里可独立复用的判断逻辑。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断图片消息是否已经超过前端允许展示的保留期。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>|null|undefined} message 聊天消息
|
||||||
|
* @param {number} retentionDays 图片保留天数
|
||||||
|
* @param {number} nowTimestamp 当前时间戳
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isExpiredChatImageMessage(
|
||||||
|
message,
|
||||||
|
retentionDays = Number.parseInt(window.chatContext?.chatImageRetentionDays || 3, 10),
|
||||||
|
nowTimestamp = Date.now(),
|
||||||
|
) {
|
||||||
|
if (!message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.message_type === "expired_image") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.message_type !== "image") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.image_url || !message.image_thumb_url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentAtText = String(message.sent_at || "").replace(" ", "T");
|
||||||
|
const sentAt = sentAtText ? new Date(sentAtText) : null;
|
||||||
|
|
||||||
|
if (!sentAt || Number.isNaN(sentAt.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nowTimestamp >= sentAt.getTime() + retentionDays * 24 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
@@ -59,6 +59,55 @@
|
|||||||
const CHAT_MESSAGE_FLUSH_BATCH_SIZE = 8;
|
const CHAT_MESSAGE_FLUSH_BATCH_SIZE = 8;
|
||||||
const PUBLIC_MESSAGE_NODE_LIMIT = 600;
|
const PUBLIC_MESSAGE_NODE_LIMIT = 600;
|
||||||
const PRIVATE_MESSAGE_NODE_LIMIT = 300;
|
const PRIVATE_MESSAGE_NODE_LIMIT = 300;
|
||||||
|
// Vite 模块稍后会覆盖这些全局工具;这里保留极小同步兜底,避免经典脚本早期事件取不到同名函数。
|
||||||
|
window.escapeHtml = window.escapeHtml || ((text) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
});
|
||||||
|
window.normalizeSafeChatUrl = window.normalizeSafeChatUrl || ((url, fallback) => {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url || fallback, window.location.origin);
|
||||||
|
|
||||||
|
if (!['http:', 'https:'].includes(parsedUrl.protocol) || parsedUrl.origin !== window.location.origin) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedUrl.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.isExpiredChatImageMessage = window.isExpiredChatImageMessage || ((msg) => {
|
||||||
|
if (!msg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message_type === 'expired_image') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message_type !== 'image') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg.image_url || !msg.image_thumb_url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retentionDays = parseInt(window.chatContext?.chatImageRetentionDays || 3, 10);
|
||||||
|
const sentAtText = String(msg.sent_at || '').replace(' ', 'T');
|
||||||
|
const sentAt = sentAtText ? new Date(sentAtText) : null;
|
||||||
|
|
||||||
|
if (!sentAt || Number.isNaN(sentAt.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date.now() >= sentAt.getTime() + retentionDays * 24 * 60 * 60 * 1000;
|
||||||
|
});
|
||||||
|
const escapeHtml = (...args) => window.escapeHtml(...args);
|
||||||
|
const normalizeSafeChatUrl = (...args) => window.normalizeSafeChatUrl(...args);
|
||||||
|
const isExpiredChatImageMessage = (...args) => window.isExpiredChatImageMessage(...args);
|
||||||
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
|
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
|
||||||
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
||||||
|
|
||||||
@@ -1349,37 +1398,6 @@
|
|||||||
|
|
||||||
window.showVipPresenceBanner = showVipPresenceBanner;
|
window.showVipPresenceBanner = showVipPresenceBanner;
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断图片消息是否已经超过前端允许展示的保留期。
|
|
||||||
*/
|
|
||||||
function isExpiredChatImageMessage(msg) {
|
|
||||||
if (!msg) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.message_type === 'expired_image') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.message_type !== 'image') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!msg.image_url || !msg.image_thumb_url) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const retentionDays = parseInt(window.chatContext?.chatImageRetentionDays || 3, 10);
|
|
||||||
const sentAtText = String(msg.sent_at || '').replace(' ', 'T');
|
|
||||||
const sentAt = sentAtText ? new Date(sentAtText) : null;
|
|
||||||
|
|
||||||
if (!sentAt || Number.isNaN(sentAt.getTime())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Date.now() >= sentAt.getTime() + retentionDays * 24 * 60 * 60 * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建普通聊天消息的正文区域,支持缩略图与过期占位渲染。
|
* 构建普通聊天消息的正文区域,支持缩略图与过期占位渲染。
|
||||||
*/
|
*/
|
||||||
@@ -3832,33 +3850,4 @@
|
|||||||
setInterval(() => saveExp(true), HEARTBEAT_INTERVAL);
|
setInterval(() => saveExp(true), HEARTBEAT_INTERVAL);
|
||||||
setTimeout(() => saveExp(true), 10000);
|
setTimeout(() => saveExp(true), 10000);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML 转义函数,防止 XSS
|
|
||||||
*/
|
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 规整广播携带的链接,只允许当前站点的 http(s) 地址进入 innerHTML。
|
|
||||||
*/
|
|
||||||
function normalizeSafeChatUrl(url, fallback) {
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url || fallback, window.location.origin);
|
|
||||||
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedUrl.origin !== window.location.origin) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedUrl.toString();
|
|
||||||
} catch (error) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user