markdown support and avoidance of XSS injection

pull/805/head
DJ2LS 2024-09-15 20:50:56 +02:00
parent 7fe714459e
commit b3ea7f2e14
4 changed files with 57 additions and 12 deletions

View File

@ -33,8 +33,10 @@
"chartjs-plugin-annotation": "^3.0.1",
"core-js": "^3.8.3",
"d3": "^7.9.0",
"dompurify": "^3.1.6",
"gridstack": "^10.3.0",
"js-image-compressor": "^2.0.0",
"marked": "^14.1.2",
"pinia": "^2.1.7",
"qth-locator": "^2.1.0",
"topojson-client": "^3.1.0",

View File

@ -26,11 +26,14 @@
</div>
<div class="card-body">
<p class="card-text text-break">{{ message.body }}</p>
<!-- Render parsed markdown with v-html -->
<p class="card-text text-break" v-html="parsedMessageBody"></p>
</div>
<div class="card-footer p-0 border-top-0">
<p class="p-0 m-0 me-1 text-end text-dark"><span class="badge badge-secondary mr-2 text-dark"> {{ getDateTime }} UTC</span></p>
<p class="p-0 m-0 me-1 text-end text-dark">
<span class="badge badge-secondary mr-2 text-dark">{{ getDateTime }} UTC</span>
</p>
<!-- Display formatted timestamp in card-footer -->
</div>
</div>
@ -39,7 +42,6 @@
<!-- Delete button outside of the card -->
<div class="col-auto">
<button
class="btn btn-outline-secondary border-0 me-1"
@click="showMessageInfo"
data-bs-target="#messageInfoModal"
@ -56,6 +58,8 @@
</template>
<script>
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import {
deleteMessageFromDB,
requestMessageInfo,
@ -64,7 +68,6 @@ import {
import chat_messages_image_preview from './chat_messages_image_preview.vue';
// Pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
@ -74,7 +77,7 @@ import { useChatStore } from '../store/chatStore.js';
const chatStore = useChatStore(pinia);
export default {
components: {
components: {
chat_messages_image_preview,
},
@ -145,6 +148,11 @@ components: {
let seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
},
parsedMessageBody() {
// Use marked to parse markdown and DOMPurify to sanitize
return DOMPurify.sanitize(marked.parse(this.message.body));
},
},
};
</script>

View File

@ -10,7 +10,6 @@
</button>
<button
class="btn btn-outline-secondary border-0 me-1"
@click="showMessageInfo"
data-bs-target="#messageInfoModal"
@ -48,7 +47,8 @@
</div>
<div class="card-body">
<p class="card-text text-break">{{ message.body }}</p>
<!-- Render parsed markdown -->
<p class="card-text text-break" v-html="parsedMessageBody"></p>
</div>
<div class="card-footer p-0 bg-secondary border-top-0">
@ -61,7 +61,7 @@
>
{{ message.status }}
</span>
| <span class="badge badge-primary mr-2" > attempt: {{ message.attempt + 1 }} </span>|<span class="badge badge-primary mr-2"> {{ getDateTime }} UTC</span>
| <span class="badge badge-primary mr-2"> attempt: {{ message.attempt + 1 }} </span>|<span class="badge badge-primary mr-2"> {{ getDateTime }} UTC</span>
</p>
</div>
@ -97,6 +97,8 @@
</template>
<script>
import { marked } from "marked";
import DOMPurify from "dompurify";
import {
repeatMessageTransmission,
deleteMessageFromDB,
@ -114,7 +116,7 @@ import { useChatStore } from '../store/chatStore.js';
const chatStore = useChatStore(pinia);
export default {
components: {
components: {
chat_messages_image_preview,
},
@ -133,7 +135,6 @@ components: {
showMessageInfo() {
chatStore.messageInfoById = requestMessageInfo(this.message.id);
},
async downloadAttachment(hash_sha512, fileName) {
@ -190,6 +191,11 @@ components: {
let seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
},
parsedMessageBody() {
// Use marked to parse markdown and DOMPurify to sanitize
return DOMPurify.sanitize(marked.parse(this.message.body));
},
},
};
</script>

View File

@ -9,6 +9,8 @@ const chat = useChatStore(pinia);
import { newMessage } from '../js/messagesHandler.js';
import { ref } from 'vue';
import { VuemojiPicker } from 'vuemoji-picker';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Emoji Handling
const handleEmojiClick = (detail) => {
@ -67,13 +69,16 @@ function transmitNewMessage() {
data: file.content,
}));
// Sanitize inputText before sending the message
const sanitizedInput = DOMPurify.sanitize(marked.parse(chat.inputText));
if (chat.selectedCallsign.startsWith("BC-")) {
return "new broadcast";
} else {
if (attachments.length > 0) {
newMessage(chat.selectedCallsign, chat.inputText, attachments);
newMessage(chat.selectedCallsign, sanitizedInput, attachments);
} else {
newMessage(chat.selectedCallsign, chat.inputText);
newMessage(chat.selectedCallsign, sanitizedInput);
}
}
@ -90,6 +95,21 @@ function resetFile() {
selectedFiles.value = [];
}
// Apply Markdown Formatting
function applyMarkdown(formatType) {
const textarea = chatModuleMessage.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = chat.inputText.substring(start, end);
if (formatType === 'bold') {
chat.inputText = chat.inputText.substring(0, start) + `**${selectedText}**` + chat.inputText.substring(end);
} else if (formatType === 'italic') {
chat.inputText = chat.inputText.substring(0, start) + `_${selectedText}_` + chat.inputText.substring(end);
} else if (formatType === 'underline') {
chat.inputText = chat.inputText.substring(0, start) + `<u>${selectedText}</u>` + chat.inputText.substring(end);
}
}
</script>
<template>
@ -141,6 +161,15 @@ function resetFile() {
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
</button>
<div class="vr mx-2"></div>
<!-- Markdown Formatting Buttons -->
<button class="btn btn-outline-secondary border-0 rounded-pill" @click="applyMarkdown('bold')"><b>B</b></button>
<button class="btn btn-outline-secondary border-0 rounded-pill" @click="applyMarkdown('italic')"><i>I</i></button>
<button class="btn btn-outline-secondary border-0 rounded-pill" @click="applyMarkdown('underline')"><u>U</u></button>
<textarea
class="form-control border rounded-pill"
rows="1"