mirror of https://github.com/DJ2LS/FreeDATA.git
markdown support and avoidance of XSS injection
parent
7fe714459e
commit
b3ea7f2e14
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue