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",
|
"chartjs-plugin-annotation": "^3.0.1",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
|
"dompurify": "^3.1.6",
|
||||||
"gridstack": "^10.3.0",
|
"gridstack": "^10.3.0",
|
||||||
"js-image-compressor": "^2.0.0",
|
"js-image-compressor": "^2.0.0",
|
||||||
|
"marked": "^14.1.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qth-locator": "^2.1.0",
|
"qth-locator": "^2.1.0",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "^3.1.0",
|
||||||
|
|
|
@ -26,11 +26,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<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>
|
||||||
|
|
||||||
<div class="card-footer p-0 border-top-0">
|
<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 -->
|
<!-- Display formatted timestamp in card-footer -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,7 +42,6 @@
|
||||||
<!-- Delete button outside of the card -->
|
<!-- Delete button outside of the card -->
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button
|
<button
|
||||||
|
|
||||||
class="btn btn-outline-secondary border-0 me-1"
|
class="btn btn-outline-secondary border-0 me-1"
|
||||||
@click="showMessageInfo"
|
@click="showMessageInfo"
|
||||||
data-bs-target="#messageInfoModal"
|
data-bs-target="#messageInfoModal"
|
||||||
|
@ -56,6 +58,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
import {
|
import {
|
||||||
deleteMessageFromDB,
|
deleteMessageFromDB,
|
||||||
requestMessageInfo,
|
requestMessageInfo,
|
||||||
|
@ -64,7 +68,6 @@ import {
|
||||||
|
|
||||||
import chat_messages_image_preview from './chat_messages_image_preview.vue';
|
import chat_messages_image_preview from './chat_messages_image_preview.vue';
|
||||||
|
|
||||||
|
|
||||||
// Pinia store setup
|
// Pinia store setup
|
||||||
import { setActivePinia } from "pinia";
|
import { setActivePinia } from "pinia";
|
||||||
import pinia from "../store/index";
|
import pinia from "../store/index";
|
||||||
|
@ -74,7 +77,7 @@ import { useChatStore } from '../store/chatStore.js';
|
||||||
const chatStore = useChatStore(pinia);
|
const chatStore = useChatStore(pinia);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
chat_messages_image_preview,
|
chat_messages_image_preview,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -145,6 +148,11 @@ components: {
|
||||||
let seconds = date.getSeconds().toString().padStart(2, "0");
|
let seconds = date.getSeconds().toString().padStart(2, "0");
|
||||||
return `${hours}:${minutes}:${seconds}`;
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parsedMessageBody() {
|
||||||
|
// Use marked to parse markdown and DOMPurify to sanitize
|
||||||
|
return DOMPurify.sanitize(marked.parse(this.message.body));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
||||||
class="btn btn-outline-secondary border-0 me-1"
|
class="btn btn-outline-secondary border-0 me-1"
|
||||||
@click="showMessageInfo"
|
@click="showMessageInfo"
|
||||||
data-bs-target="#messageInfoModal"
|
data-bs-target="#messageInfoModal"
|
||||||
|
@ -48,7 +47,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<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>
|
||||||
|
|
||||||
<div class="card-footer p-0 bg-secondary border-top-0">
|
<div class="card-footer p-0 bg-secondary border-top-0">
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
>
|
>
|
||||||
{{ message.status }}
|
{{ message.status }}
|
||||||
</span>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -97,6 +97,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { marked } from "marked";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
import {
|
import {
|
||||||
repeatMessageTransmission,
|
repeatMessageTransmission,
|
||||||
deleteMessageFromDB,
|
deleteMessageFromDB,
|
||||||
|
@ -114,7 +116,7 @@ import { useChatStore } from '../store/chatStore.js';
|
||||||
const chatStore = useChatStore(pinia);
|
const chatStore = useChatStore(pinia);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
chat_messages_image_preview,
|
chat_messages_image_preview,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -133,7 +135,6 @@ components: {
|
||||||
|
|
||||||
showMessageInfo() {
|
showMessageInfo() {
|
||||||
chatStore.messageInfoById = requestMessageInfo(this.message.id);
|
chatStore.messageInfoById = requestMessageInfo(this.message.id);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async downloadAttachment(hash_sha512, fileName) {
|
async downloadAttachment(hash_sha512, fileName) {
|
||||||
|
@ -190,6 +191,11 @@ components: {
|
||||||
let seconds = date.getSeconds().toString().padStart(2, "0");
|
let seconds = date.getSeconds().toString().padStart(2, "0");
|
||||||
return `${hours}:${minutes}:${seconds}`;
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parsedMessageBody() {
|
||||||
|
// Use marked to parse markdown and DOMPurify to sanitize
|
||||||
|
return DOMPurify.sanitize(marked.parse(this.message.body));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,6 +9,8 @@ const chat = useChatStore(pinia);
|
||||||
import { newMessage } from '../js/messagesHandler.js';
|
import { newMessage } from '../js/messagesHandler.js';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { VuemojiPicker } from 'vuemoji-picker';
|
import { VuemojiPicker } from 'vuemoji-picker';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
// Emoji Handling
|
// Emoji Handling
|
||||||
const handleEmojiClick = (detail) => {
|
const handleEmojiClick = (detail) => {
|
||||||
|
@ -67,13 +69,16 @@ function transmitNewMessage() {
|
||||||
data: file.content,
|
data: file.content,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Sanitize inputText before sending the message
|
||||||
|
const sanitizedInput = DOMPurify.sanitize(marked.parse(chat.inputText));
|
||||||
|
|
||||||
if (chat.selectedCallsign.startsWith("BC-")) {
|
if (chat.selectedCallsign.startsWith("BC-")) {
|
||||||
return "new broadcast";
|
return "new broadcast";
|
||||||
} else {
|
} else {
|
||||||
if (attachments.length > 0) {
|
if (attachments.length > 0) {
|
||||||
newMessage(chat.selectedCallsign, chat.inputText, attachments);
|
newMessage(chat.selectedCallsign, sanitizedInput, attachments);
|
||||||
} else {
|
} else {
|
||||||
newMessage(chat.selectedCallsign, chat.inputText);
|
newMessage(chat.selectedCallsign, sanitizedInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +95,21 @@ function resetFile() {
|
||||||
selectedFiles.value = [];
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -141,6 +161,15 @@ function resetFile() {
|
||||||
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
|
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
|
||||||
</button>
|
</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
|
<textarea
|
||||||
class="form-control border rounded-pill"
|
class="form-control border rounded-pill"
|
||||||
rows="1"
|
rows="1"
|
||||||
|
|
Loading…
Reference in New Issue