Avoid rendering HTML, detect links explicitly

This commit is contained in:
Eden Yemini 2025-01-27 04:18:06 +02:00
parent 1502632433
commit 1a3150351d
2 changed files with 71 additions and 31 deletions

View file

@ -0,0 +1,65 @@
<template>
<span>
<template v-for="(part, index) in parts" :key="index">
<a
v-if="part.type === 'link'"
:href="part.content"
target="_blank"
rel="noopener noreferrer"
>{{ part.content }}</a>
<span v-else>{{ part.content }}</span>
</template>
</span>
</template>
<script>
export default {
name: "SafeLinks",
props: {
text: {
type: String,
required: true
}
},
computed: {
parts() {
if (!this.text) {
return [];
}
const urlPattern = /(\b(?:https?|ftp|file|smb|ssh|telnet|ldap|git):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
const parts = [];
let lastIndex = 0;
this.text.replace(urlPattern, (match, url, offset) => {
// Add text before the link
if (offset > lastIndex) {
parts.push({
type: "text",
content: this.text.slice(lastIndex, offset)
});
}
// Add the link
parts.push({
type: "link",
content: url
});
lastIndex = offset + match.length;
});
// Add remaining text after last link
if (lastIndex < this.text.length) {
parts.push({
type: "text",
content: this.text.slice(lastIndex)
});
}
return parts;
}
}
};
</script>

View file

@ -9,9 +9,9 @@
<div>{{ monitor.id }}</div> <div>{{ monitor.id }}</div>
</div> </div>
</h1> </h1>
<!-- eslint-disable vue/no-v-html --> <p v-if="monitor.description">
<p v-if="monitor.description" v-html="processedDescription"></p> <SafeLinks :text="monitor.description" />
<!--eslint-enable--> </p>
<div class="d-flex"> <div class="d-flex">
<div class="tags"> <div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
@ -281,6 +281,7 @@ import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue"; import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue"; import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import SafeLinks from "../components/SafeLinks.vue";
import Pagination from "v-pagination-3"; import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue")); const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
@ -309,7 +310,8 @@ export default {
Tag, Tag,
CertificateInfo, CertificateInfo,
PrismEditor, PrismEditor,
ScreenshotDialog ScreenshotDialog,
SafeLinks,
}, },
data() { data() {
return { return {
@ -401,19 +403,6 @@ export default {
screenshotURL() { screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime; return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
},
processedDescription() {
if (!this.monitor.description) {
return "";
}
const urlPattern = /(\b(?:https?|ftp|file|smb|ssh|telnet|ldap|git):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
const processed = this.monitor.description.replace(
urlPattern,
url => `<a href="${this.escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${this.escapeHtml(url)}</a>`
);
return processed;
} }
}, },
@ -671,20 +660,6 @@ export default {
.replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL); .replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL);
this.pushMonitor.code = code; this.pushMonitor.code = code;
}); });
},
/**
* Escape HTML
* @param {string} unsafe Unsafe string
* @returns {string} Safe string
*/
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
} }
}, },
}; };