diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index cb480d3..710f20d 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -11,7 +11,9 @@ "@fontsource/roboto": "^5.2.9", "@mdi/font": "^7.4.47", "buffer": "^6.0.3", + "dompurify": "^3.3.1", "irc-framework": "^4.14.0", + "markdown-it": "^14.1.0", "pinia": "^3.0.4", "vite-plugin-node-polyfills": "^0.25.0", "vue": "^3.2.37", @@ -20,6 +22,8 @@ "devDependencies": { "@babel/types": "^7.18.10", "@tsconfig/node22": "^22.0.5", + "@types/dompurify": "^3.0.5", + "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^6.0.3", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", @@ -1569,6 +1573,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1583,6 +1597,38 @@ "license": "MIT", "peer": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", @@ -2127,7 +2173,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asn1.js": { @@ -2999,6 +3044,15 @@ "url": "https://bevry.me/fund" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4542,6 +4596,15 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/local-pkg": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", @@ -4597,6 +4660,35 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4617,6 +4709,12 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/middleware-handler": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/middleware-handler/-/middleware-handler-0.2.0.tgz", @@ -5308,6 +5406,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -6535,6 +6642,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index 6f572d7..f3ac2bd 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -12,7 +12,9 @@ "@fontsource/roboto": "^5.2.9", "@mdi/font": "^7.4.47", "buffer": "^6.0.3", + "dompurify": "^3.3.1", "irc-framework": "^4.14.0", + "markdown-it": "^14.1.0", "pinia": "^3.0.4", "vite-plugin-node-polyfills": "^0.25.0", "vue": "^3.2.37", @@ -21,6 +23,8 @@ "devDependencies": { "@babel/types": "^7.18.10", "@tsconfig/node22": "^22.0.5", + "@types/dompurify": "^3.0.5", + "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^6.0.3", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", diff --git a/app/frontend/src/components/MessageList.vue b/app/frontend/src/components/MessageList.vue index 47f1210..bbb132d 100644 --- a/app/frontend/src/components/MessageList.vue +++ b/app/frontend/src/components/MessageList.vue @@ -61,9 +61,8 @@ watch( >mdi-eyeOnly visible to you -
{{ msg.message }}
- +
diff --git a/app/frontend/src/stores/irc.ts b/app/frontend/src/stores/irc.ts index 43c9c5b..43f5e27 100644 --- a/app/frontend/src/stores/irc.ts +++ b/app/frontend/src/stores/irc.ts @@ -4,6 +4,8 @@ import { Client } from "irc-framework"; import { useBufferStore } from "./bufferStore"; import { ref } from "vue"; import { useAccountStore } from "./accountStore"; +import MarkdownIt from "markdown-it"; +import DOMPurify from "dompurify"; export type HookFunction = (event: any) => HookStatus; @@ -19,6 +21,39 @@ interface Batch { params: string[]; } +const md = new MarkdownIt({ + html: false, + linkify: true, + typographer: true, +}); + +function renderMessage(msgIn: string) { + if (!msgIn) return ""; + const dirty = md.render(msgIn); + return DOMPurify.sanitize(dirty, { + ALLOWED_TAGS: [ + "li", + "ul", + "ol", + "b", + "i", + "em", + "strong", + "a", + "code", + "pre", + "br", + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + ], + ALLOWED_ATTR: ["href", "target", "class"], + }); +} + export const useIRCStore = defineStore("ircStore", () => { const bufferStore = useBufferStore(); const accountStore = useAccountStore(); @@ -289,6 +324,7 @@ export const useIRCStore = defineStore("ircStore", () => { }); } + message.message = renderMessage(message.message); buffer.messages.push(message); if (