Skip to content

Commit 7a30bd5

Browse files
authored
Fix #374 and #409 - Prevent XSS and add Markdown support (#448)
1 parent 863568a commit 7a30bd5

File tree

11 files changed

+458
-346
lines changed

11 files changed

+458
-346
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
},
6767
"dependencies": {
6868
"emoji-picker-element": "1.12.1",
69-
"linkifyjs": "2.1.9"
69+
"micromark": "^3.1.0",
70+
"micromark-extension-gfm": "^2.0.1"
7071
}
7172
}

src/components/FormatMessage/FormatMessage.vue

Lines changed: 36 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,27 @@
33
class="vac-format-message-wrapper"
44
:class="{ 'vac-text-ellipsis': singleLine }"
55
>
6-
<div
7-
v-if="!textFormatting.disabled"
8-
:class="{ 'vac-text-ellipsis': singleLine }"
9-
>
6+
<template v-for="(message, i) in parsedMessage" :key="i">
107
<div
11-
v-for="(message, i) in linkifiedMessage"
12-
:key="i"
8+
v-if="message.markdown"
9+
class="markdown"
10+
@click="openTag"
11+
v-html="message.value"
12+
/>
13+
<div
14+
v-else
1315
class="vac-format-container"
16+
:class="{ 'vac-text-ellipsis': singleLine }"
1417
>
1518
<component
1619
:is="message.url ? 'a' : 'span'"
1720
:class="{
1821
'vac-text-ellipsis': singleLine,
19-
'vac-text-bold': message.bold,
20-
'vac-text-italic': deleted || message.italic,
21-
'vac-text-strike': message.strike,
22-
'vac-text-underline': message.underline,
23-
'vac-text-inline-code': !singleLine && message.inline,
24-
'vac-text-multiline-code': !singleLine && message.multiline,
2522
'vac-text-tag': !singleLine && !reply && message.tag
2623
}"
2724
:href="message.href"
2825
:target="message.href ? linkOptions.target : null"
2926
:rel="message.href ? linkOptions.rel : null"
30-
@click="openTag(message)"
3127
>
3228
<template v-if="deleted">
3329
<slot
@@ -56,23 +52,22 @@
5652
/>
5753
</div>
5854
<div class="vac-image-link-message">
59-
<span>{{ message.value }}</span>
55+
{{ message.value }}
6056
</div>
6157
</template>
6258
<template v-else>
63-
<span v-html="message.value" />
59+
{{ message.value }}
6460
</template>
6561
</component>
6662
</div>
67-
</div>
68-
<div v-else v-html="formattedContent" />
63+
</template>
6964
</div>
7065
</template>
7166

7267
<script>
7368
import SvgIcon from '../SvgIcon/SvgIcon'
7469
75-
import formatString from '../../utils/format-string'
70+
import markdown from '../../utils/markdown'
7671
import { IMAGE_TYPES } from '../../utils/constants'
7772
7873
export default {
@@ -97,38 +92,36 @@ export default {
9792
emits: ['open-user-tag'],
9893
9994
computed: {
100-
linkifiedMessage() {
95+
parsedMessage() {
10196
if (this.deleted) {
10297
return [{ value: this.textMessages.MESSAGE_DELETED }]
10398
}
10499
105-
const message = formatString(
106-
this.formatTags(this.content),
107-
this.linkify && !this.linkOptions.disabled,
108-
this.textFormatting
109-
)
100+
let options
101+
if (!this.textFormatting.disabled) {
102+
options = {
103+
textFormatting: {
104+
linkify: this.linkify,
105+
linkOptions: this.linkOptions,
106+
singleLine: this.singleLine,
107+
reply: this.reply,
108+
users: this.users,
109+
...this.textFormatting
110+
}
111+
}
112+
} else {
113+
options = {}
114+
}
115+
116+
const message = markdown(this.content, options)
110117
111118
message.forEach(m => {
112-
m.url = this.checkType(m, 'url')
113-
m.bold = this.checkType(m, 'bold')
114-
m.italic = this.checkType(m, 'italic')
115-
m.strike = this.checkType(m, 'strike')
116-
m.underline = this.checkType(m, 'underline')
117-
m.inline = this.checkType(m, 'inline-code')
118-
m.multiline = this.checkType(m, 'multiline-code')
119+
m.markdown = this.checkType(m, 'markdown')
119120
m.tag = this.checkType(m, 'tag')
120121
m.image = this.checkImageType(m)
121-
m.value = this.replaceEmojiByElement(m.value)
122122
})
123123
124124
return message
125-
},
126-
formattedContent() {
127-
if (this.deleted) {
128-
return this.textMessages.MESSAGE_DELETED
129-
} else {
130-
return this.formatTags(this.content)
131-
}
132125
}
133126
},
134127
@@ -162,63 +155,12 @@ export default {
162155
image.removeEventListener('load', onLoad)
163156
}
164157
},
165-
formatTags(content) {
166-
const firstTag = '<usertag>'
167-
const secondTag = '</usertag>'
168-
169-
const usertags = [...content.matchAll(new RegExp(firstTag, 'gi'))].map(
170-
a => a.index
171-
)
172-
173-
const initialContent = content
174-
175-
usertags.forEach(index => {
176-
const userId = initialContent.substring(
177-
index + firstTag.length,
178-
initialContent.indexOf(secondTag, index)
179-
)
180-
181-
const user = this.users.find(user => user._id === userId)
182-
183-
content = content.replaceAll(userId, `@${user?.username || 'unknown'}`)
184-
})
185-
186-
return content
187-
},
188-
openTag(message) {
189-
if (!this.singleLine && this.checkType(message, 'tag')) {
190-
const user = this.users.find(
191-
u => message.value.indexOf(u.username) !== -1
192-
)
158+
openTag(event) {
159+
const userId = event.target.getAttribute('data-user-id')
160+
if (!this.singleLine && userId) {
161+
const user = this.users.find(u => String(u._id) === userId)
193162
this.$emit('open-user-tag', user)
194163
}
195-
},
196-
replaceEmojiByElement(value) {
197-
let emojiSize
198-
if (this.singleLine) {
199-
emojiSize = 16
200-
} else {
201-
const onlyEmojis = this.containsOnlyEmojis()
202-
emojiSize = onlyEmojis ? 28 : 20
203-
}
204-
205-
return value.replaceAll(
206-
/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/gu,
207-
v => {
208-
return `<span style="font-size: ${emojiSize}px">${v}</span>`
209-
}
210-
)
211-
},
212-
containsOnlyEmojis() {
213-
const onlyEmojis = this.content.replace(
214-
new RegExp('[\u0000-\u1eeff]', 'g'),
215-
''
216-
)
217-
const visibleChars = this.content.replace(
218-
new RegExp('[\n\rs]+|( )+', 'g'),
219-
''
220-
)
221-
return onlyEmojis.length === visibleChars.length
222164
}
223165
}
224166
}

src/lib/ChatWindow.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,7 @@ export default {
183183
textFormatting: {
184184
type: [Object, String],
185185
default: () => ({
186-
disabled: false,
187-
italic: '_',
188-
bold: '*',
189-
strike: '~',
190-
underline: '°',
191-
multilineCode: '```',
192-
inlineCode: '`'
186+
disabled: false
193187
})
194188
},
195189
linkOptions: {

src/styles/helper.scss

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -84,44 +84,6 @@
8484
text-overflow: ellipsis;
8585
}
8686

87-
.vac-text-bold {
88-
font-weight: bold;
89-
}
90-
91-
.vac-text-italic {
92-
font-style: italic;
93-
}
94-
95-
.vac-text-strike {
96-
text-decoration: line-through;
97-
}
98-
99-
.vac-text-underline {
100-
text-decoration: underline;
101-
}
102-
103-
.vac-text-inline-code {
104-
display: inline-block;
105-
font-size: 12px;
106-
color: var(--chat-markdown-color);
107-
background: var(--chat-markdown-bg);
108-
border: 1px solid var(--chat-markdown-border);
109-
border-radius: 3px;
110-
margin: 2px 0;
111-
padding: 2px 3px;
112-
}
113-
114-
.vac-text-multiline-code {
115-
display: block;
116-
font-size: 12px;
117-
color: var(--chat-markdown-color-multi);
118-
background: var(--chat-markdown-bg);
119-
border: 1px solid var(--chat-markdown-border);
120-
border-radius: 3px;
121-
margin: 4px 0;
122-
padding: 7px;
123-
}
124-
12587
.vac-text-tag {
12688
color: var(--chat-message-color-tag);
12789
cursor: pointer;

src/styles/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@import 'animation';
22
@import 'menu';
33
@import 'helper';
4+
@import 'markdown';
45

56
@import '../lib/ChatWindow';
67

src/styles/markdown.scss

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.markdown {
2+
p {
3+
margin: 0;
4+
}
5+
6+
ol {
7+
display: flex;
8+
flex-direction: column;
9+
list-style-position: inside;
10+
}
11+
12+
ul {
13+
display: flex;
14+
flex-direction: column;
15+
}
16+
17+
code {
18+
display: block;
19+
font-size: 12px;
20+
color: var(--chat-markdown-color-multi);
21+
background: var(--chat-markdown-bg);
22+
border: 1px solid var(--chat-markdown-border);
23+
border-radius: 3px;
24+
margin: 4px 0;
25+
padding: 7px;
26+
}
27+
28+
p {
29+
code {
30+
display: inline-block;
31+
font-size: 12px;
32+
color: var(--chat-markdown-color);
33+
background: var(--chat-markdown-bg);
34+
border: 1px solid var(--chat-markdown-border);
35+
border-radius: 3px;
36+
margin: 2px 0;
37+
padding: 2px 3px;
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)