mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 02:12:25 +00:00
sanitize links, include mentioned members
This commit is contained in:
1 parent
53cea75afd
commit
7a3b83a660
3 files changed
+27
-24
No files matched your search
+9
-15
@@ -14,7 +14,6 @@ module Simplex.Chat.Web
|
||||
WebMemberProfile (..),
|
||||
WebFileInfo (..),
|
||||
webPreviewWorker,
|
||||
renderWebPreviews,
|
||||
writeCorsConfig,
|
||||
removeStaleFiles,
|
||||
channelContentChanged,
|
||||
@@ -60,7 +59,7 @@ import Simplex.Chat.Messages
|
||||
)
|
||||
import Simplex.Chat.Messages.CIContent (ciMsgContent)
|
||||
import Simplex.Chat.Protocol (MsgContent, MsgRef (..), QuotedMsg (..), isReport)
|
||||
import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups, getRelayServedGroups)
|
||||
import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups)
|
||||
import Simplex.Chat.Store.Messages (getGroupWebPreviewItems)
|
||||
import Simplex.Chat.Store.Shared (getGroupInfo)
|
||||
import Simplex.Chat.Types
|
||||
@@ -230,14 +229,6 @@ webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterva
|
||||
Right a -> pure (Just a)
|
||||
Left _ -> go us
|
||||
|
||||
renderWebPreviews :: WebPreviewConfig -> ChatController -> User -> IO ()
|
||||
renderWebPreviews cfg@WebPreviewConfig {webJsonDir} cc user = do
|
||||
let vr' = chatVRange (config cc)
|
||||
createDirectoryIfMissing True webJsonDir
|
||||
groups <- withTransaction (chatStore cc) $ \db -> getRelayServedGroups db vr' user
|
||||
forM_ [gInfo | gInfo <- groups, hasPublicGroup gInfo] $ \gInfo ->
|
||||
void $ renderGroupPreview cfg cc user gInfo
|
||||
|
||||
renderGroupPreview :: WebPreviewConfig -> ChatController -> User -> GroupInfo -> IO (Maybe (Text, CorsOrigin))
|
||||
renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gInfo@GroupInfo {groupProfile = gp@GroupProfile {shortDescr = sd, description = wd, publicGroup}} =
|
||||
case publicGroup of
|
||||
@@ -250,7 +241,7 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI
|
||||
ts <- getCurrentTime
|
||||
let rendered = mapMaybe toRenderedItem $ rights items
|
||||
msgs = map fst rendered
|
||||
senders = collectSenders $ map memberToProfile owners <> mapMaybe snd rendered
|
||||
senders = collectSenders $ map memberToProfile owners <> concatMap snd rendered
|
||||
preview = WebChannelPreview
|
||||
{ channel = gp,
|
||||
shortDescription = toFormattedText =<< sd,
|
||||
@@ -309,14 +300,17 @@ channelRemoved cc gId =
|
||||
writeTVar corsNeeded True
|
||||
void $ tryPutTMVar wakeSignal ()
|
||||
|
||||
toRenderedItem :: CChatItem 'CTGroup -> Maybe (WebMessage, Maybe WebMemberProfile)
|
||||
toRenderedItem :: CChatItem 'CTGroup -> Maybe (WebMessage, [WebMemberProfile])
|
||||
toRenderedItem (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemTimed, itemForwarded, itemEdited}, content, mentions, formattedText, quotedItem, reactions, file})
|
||||
| isJust itemTimed = Nothing
|
||||
| otherwise = case ciMsgContent content of
|
||||
Just mc | not (isReport mc) ->
|
||||
let (sender, senderProfile) = case chatDir of
|
||||
CIGroupRcv m@GroupMember {memberId} -> (Just memberId, Just $ memberToProfile m)
|
||||
_ -> (Nothing, Nothing)
|
||||
CIGroupRcv m@GroupMember {memberId} -> (Just memberId, [memberToProfile m])
|
||||
_ -> (Nothing, [])
|
||||
quotedProfile = case quotedItem of
|
||||
Just CIQuote {chatDir = CIQGroupRcv (Just m)} -> [memberToProfile m]
|
||||
_ -> []
|
||||
in Just
|
||||
( WebMessage
|
||||
{ sender,
|
||||
@@ -330,7 +324,7 @@ toRenderedItem (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemTimed,
|
||||
forward = if isJust itemForwarded then Just True else Nothing,
|
||||
edited = itemEdited
|
||||
},
|
||||
senderProfile
|
||||
senderProfile <> quotedProfile
|
||||
)
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
@@ -1185,7 +1185,7 @@ function renderMessages(container, messages, membersMap, channel) {
|
||||
}
|
||||
|
||||
const col = document.createElement('div');
|
||||
const bubble = renderBubble(msg, member, showTail);
|
||||
const bubble = renderBubble(msg, member, showTail, membersMap);
|
||||
col.appendChild(bubble);
|
||||
|
||||
if (msg.reactions && msg.reactions.length > 0) {
|
||||
@@ -1198,7 +1198,7 @@ function renderMessages(container, messages, membersMap, channel) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderBubble(msg, member, showTail) {
|
||||
function renderBubble(msg, member, showTail, membersMap) {
|
||||
const mc = msg.content;
|
||||
const mediaOnly = (mc.type === 'image' || mc.type === 'video') && !mc.text && !msg.quote && !msg.forward;
|
||||
const noTailContent = (mc.type === 'image' || mc.type === 'video' || mc.type === 'voice') && !mc.text;
|
||||
@@ -1225,7 +1225,7 @@ function renderBubble(msg, member, showTail) {
|
||||
}
|
||||
|
||||
if (msg.quote) {
|
||||
inner.appendChild(renderQuote(msg.quote));
|
||||
inner.appendChild(renderQuote(msg.quote, membersMap));
|
||||
}
|
||||
|
||||
switch (mc.type) {
|
||||
@@ -1262,7 +1262,7 @@ function renderBubble(msg, member, showTail) {
|
||||
return bubble;
|
||||
}
|
||||
|
||||
function renderQuote(quote) {
|
||||
function renderQuote(quote, membersMap) {
|
||||
const quoteDiv = document.createElement('div');
|
||||
quoteDiv.className = 'simplex-preview-quote';
|
||||
|
||||
@@ -1272,7 +1272,8 @@ function renderQuote(quote) {
|
||||
if (quote.msgRef && quote.msgRef.memberId) {
|
||||
const sender = document.createElement('div');
|
||||
sender.className = 'simplex-preview-quote-sender';
|
||||
sender.textContent = quote.msgRef.sent ? 'You' : (quote.msgRef.memberId || '');
|
||||
const quotedMember = membersMap[quote.msgRef.memberId];
|
||||
sender.textContent = quote.msgRef.sent ? 'You' : (quotedMember ? quotedMember.displayName : '');
|
||||
contentDiv.appendChild(sender);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ function escapeAttr(text) {
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
const SAFE_URI_SCHEME = /^(https?:|simplex:|mailto:|tel:)/i;
|
||||
|
||||
function safeHref(uri) {
|
||||
if (SAFE_URI_SCHEME.test(uri)) return escapeAttr(uri);
|
||||
return `javascript:void(alert('Potentially malicious link blocked:\\n'+${JSON.stringify(uri)}))`;
|
||||
}
|
||||
|
||||
function getSimplexLinkDescr(linkType) {
|
||||
switch (linkType) {
|
||||
case 'contact': return 'SimpleX contact address';
|
||||
@@ -78,19 +85,20 @@ function renderMarkdown(fts) {
|
||||
case 'colored':
|
||||
html += `<span class="${(format.color || '').replace(/[^a-zA-Z0-9_-]/g, '')}">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'uri':
|
||||
case 'uri': {
|
||||
let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
|
||||
html += `<a href="${escapeAttr(href)}"${targetBlank(href)}>${escapeHtml(text)}</a>`;
|
||||
html += `<a href="${safeHref(href)}"${targetBlank(href)}>${escapeHtml(text)}</a>`;
|
||||
break;
|
||||
}
|
||||
case 'hyperLink': {
|
||||
const { showText, linkUri } = format;
|
||||
html += `<a href="${escapeAttr(linkUri)}"${targetBlank(linkUri)}>${escapeHtml(showText ?? linkUri)}</a>`;
|
||||
html += `<a href="${safeHref(linkUri)}"${targetBlank(linkUri)}>${escapeHtml(showText ?? linkUri)}</a>`;
|
||||
break;
|
||||
}
|
||||
case 'simplexLink': {
|
||||
const { showText, linkType, simplexUri, smpHosts } = format;
|
||||
const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType);
|
||||
html += `<a href="${escapeAttr(platformSimplexUri(simplexUri))}" target="_blank">${linkText} <em>(${escapeHtml(viaHost(smpHosts))})</em></a>`;
|
||||
html += `<a href="${safeHref(platformSimplexUri(simplexUri))}" target="_blank">${linkText} <em>(${escapeHtml(viaHost(smpHosts))})</em></a>`;
|
||||
break;
|
||||
}
|
||||
case 'command':
|
||||
|
||||
Reference in new issue
Block a user