Skip to content

Commit

Permalink
Fix #374 and #409 - Prevent XSS and add Markdown support (#448)
Browse files Browse the repository at this point in the history
  • Loading branch information
aerovulpe authored May 5, 2023
1 parent 863568a commit 7a30bd5
Show file tree
Hide file tree
Showing 11 changed files with 458 additions and 346 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
},
"dependencies": {
"emoji-picker-element": "1.12.1",
"linkifyjs": "2.1.9"
"micromark": "^3.1.0",
"micromark-extension-gfm": "^2.0.1"
}
}
130 changes: 36 additions & 94 deletions src/components/FormatMessage/FormatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,27 @@
class="vac-format-message-wrapper"
:class="{ 'vac-text-ellipsis': singleLine }"
>
<div
v-if="!textFormatting.disabled"
:class="{ 'vac-text-ellipsis': singleLine }"
>
<template v-for="(message, i) in parsedMessage" :key="i">
<div
v-for="(message, i) in linkifiedMessage"
:key="i"
v-if="message.markdown"
class="markdown"
@click="openTag"
v-html="message.value"
/>
<div
v-else
class="vac-format-container"
:class="{ 'vac-text-ellipsis': singleLine }"
>
<component
:is="message.url ? 'a' : 'span'"
:class="{
'vac-text-ellipsis': singleLine,
'vac-text-bold': message.bold,
'vac-text-italic': deleted || message.italic,
'vac-text-strike': message.strike,
'vac-text-underline': message.underline,
'vac-text-inline-code': !singleLine && message.inline,
'vac-text-multiline-code': !singleLine && message.multiline,
'vac-text-tag': !singleLine && !reply && message.tag
}"
:href="message.href"
:target="message.href ? linkOptions.target : null"
:rel="message.href ? linkOptions.rel : null"
@click="openTag(message)"
>
<template v-if="deleted">
<slot
Expand Down Expand Up @@ -56,23 +52,22 @@
/>
</div>
<div class="vac-image-link-message">
<span>{{ message.value }}</span>
{{ message.value }}
</div>
</template>
<template v-else>
<span v-html="message.value" />
{{ message.value }}
</template>
</component>
</div>
</div>
<div v-else v-html="formattedContent" />
</template>
</div>
</template>

<script>
import SvgIcon from '../SvgIcon/SvgIcon'
import formatString from '../../utils/format-string'
import markdown from '../../utils/markdown'
import { IMAGE_TYPES } from '../../utils/constants'
export default {
Expand All @@ -97,38 +92,36 @@ export default {
emits: ['open-user-tag'],
computed: {
linkifiedMessage() {
parsedMessage() {
if (this.deleted) {
return [{ value: this.textMessages.MESSAGE_DELETED }]
}
const message = formatString(
this.formatTags(this.content),
this.linkify && !this.linkOptions.disabled,
this.textFormatting
)
let options
if (!this.textFormatting.disabled) {
options = {
textFormatting: {
linkify: this.linkify,
linkOptions: this.linkOptions,
singleLine: this.singleLine,
reply: this.reply,
users: this.users,
...this.textFormatting
}
}
} else {
options = {}
}
const message = markdown(this.content, options)
message.forEach(m => {
m.url = this.checkType(m, 'url')
m.bold = this.checkType(m, 'bold')
m.italic = this.checkType(m, 'italic')
m.strike = this.checkType(m, 'strike')
m.underline = this.checkType(m, 'underline')
m.inline = this.checkType(m, 'inline-code')
m.multiline = this.checkType(m, 'multiline-code')
m.markdown = this.checkType(m, 'markdown')
m.tag = this.checkType(m, 'tag')
m.image = this.checkImageType(m)
m.value = this.replaceEmojiByElement(m.value)
})
return message
},
formattedContent() {
if (this.deleted) {
return this.textMessages.MESSAGE_DELETED
} else {
return this.formatTags(this.content)
}
}
},
Expand Down Expand Up @@ -162,63 +155,12 @@ export default {
image.removeEventListener('load', onLoad)
}
},
formatTags(content) {
const firstTag = '<usertag>'
const secondTag = '</usertag>'
const usertags = [...content.matchAll(new RegExp(firstTag, 'gi'))].map(
a => a.index
)
const initialContent = content
usertags.forEach(index => {
const userId = initialContent.substring(
index + firstTag.length,
initialContent.indexOf(secondTag, index)
)
const user = this.users.find(user => user._id === userId)
content = content.replaceAll(userId, `@${user?.username || 'unknown'}`)
})
return content
},
openTag(message) {
if (!this.singleLine && this.checkType(message, 'tag')) {
const user = this.users.find(
u => message.value.indexOf(u.username) !== -1
)
openTag(event) {
const userId = event.target.getAttribute('data-user-id')
if (!this.singleLine && userId) {
const user = this.users.find(u => String(u._id) === userId)
this.$emit('open-user-tag', user)
}
},
replaceEmojiByElement(value) {
let emojiSize
if (this.singleLine) {
emojiSize = 16
} else {
const onlyEmojis = this.containsOnlyEmojis()
emojiSize = onlyEmojis ? 28 : 20
}
return value.replaceAll(
/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/gu,
v => {
return `<span style="font-size: ${emojiSize}px">${v}</span>`
}
)
},
containsOnlyEmojis() {
const onlyEmojis = this.content.replace(
new RegExp('[\u0000-\u1eeff]', 'g'),
''
)
const visibleChars = this.content.replace(
new RegExp('[\n\rs]+|( )+', 'g'),
''
)
return onlyEmojis.length === visibleChars.length
}
}
}
Expand Down
8 changes: 1 addition & 7 deletions src/lib/ChatWindow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,7 @@ export default {
textFormatting: {
type: [Object, String],
default: () => ({
disabled: false,
italic: '_',
bold: '*',
strike: '~',
underline: '°',
multilineCode: '```',
inlineCode: '`'
disabled: false
})
},
linkOptions: {
Expand Down
38 changes: 0 additions & 38 deletions src/styles/helper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,44 +84,6 @@
text-overflow: ellipsis;
}

.vac-text-bold {
font-weight: bold;
}

.vac-text-italic {
font-style: italic;
}

.vac-text-strike {
text-decoration: line-through;
}

.vac-text-underline {
text-decoration: underline;
}

.vac-text-inline-code {
display: inline-block;
font-size: 12px;
color: var(--chat-markdown-color);
background: var(--chat-markdown-bg);
border: 1px solid var(--chat-markdown-border);
border-radius: 3px;
margin: 2px 0;
padding: 2px 3px;
}

.vac-text-multiline-code {
display: block;
font-size: 12px;
color: var(--chat-markdown-color-multi);
background: var(--chat-markdown-bg);
border: 1px solid var(--chat-markdown-border);
border-radius: 3px;
margin: 4px 0;
padding: 7px;
}

.vac-text-tag {
color: var(--chat-message-color-tag);
cursor: pointer;
Expand Down
1 change: 1 addition & 0 deletions src/styles/index.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import 'animation';
@import 'menu';
@import 'helper';
@import 'markdown';

@import '../lib/ChatWindow';

Expand Down
40 changes: 40 additions & 0 deletions src/styles/markdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.markdown {
p {
margin: 0;
}

ol {
display: flex;
flex-direction: column;
list-style-position: inside;
}

ul {
display: flex;
flex-direction: column;
}

code {
display: block;
font-size: 12px;
color: var(--chat-markdown-color-multi);
background: var(--chat-markdown-bg);
border: 1px solid var(--chat-markdown-border);
border-radius: 3px;
margin: 4px 0;
padding: 7px;
}

p {
code {
display: inline-block;
font-size: 12px;
color: var(--chat-markdown-color);
background: var(--chat-markdown-bg);
border: 1px solid var(--chat-markdown-border);
border-radius: 3px;
margin: 2px 0;
padding: 2px 3px;
}
}
}
Loading

0 comments on commit 7a30bd5

Please sign in to comment.