diff --git a/apps/frontend/src/components/ui/servers/PanelTerminal.vue b/apps/frontend/src/components/ui/servers/PanelTerminal.vue index 138baa507..02d7edb90 100644 --- a/apps/frontend/src/components/ui/servers/PanelTerminal.vue +++ b/apps/frontend/src/components/ui/servers/PanelTerminal.vue @@ -260,7 +260,25 @@
-
{{ selectedLog }}
+

+        
+

Detected Links

+ +
@@ -272,6 +290,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; import { useDebounceFn } from "@vueuse/core"; import { NewModal } from "@modrinth/ui"; import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue"; +import DOMPurify from "dompurify"; import { usePyroConsole } from "~/store/console.ts"; const { $cosmetics } = useNuxtApp(); @@ -984,6 +1003,38 @@ const jumpToLine = (line: string, event?: MouseEvent) => { }); }; +const sanitizeUrl = (url: string): string => { + try { + const parsed = new URL(url); + if (!["http:", "https:"].includes(parsed.protocol)) { + return "#"; + } + return parsed.toString(); + } catch { + return "#"; + } +}; + +const detectedLinks = computed(() => { + const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g; + const matches = [...selectedLog.value.matchAll(urlRegex)].map((match) => match[0]); + return matches.filter((url) => sanitizeUrl(url) !== "#"); +}); + +const processedLogWithLinks = computed(() => { + const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g; + const sanitizedLog = DOMPurify.sanitize(selectedLog.value, { + ALLOWED_TAGS: [], + ALLOWED_ATTR: [], + }); + + return sanitizedLog.replace(urlRegex, (url) => { + const safeUrl = sanitizeUrl(url); + if (safeUrl === "#") return url; + return `${url}`; + }); +}); + watch( () => pyroConsole.filteredOutput.value, () => {