diff --git a/package-lock.json b/package-lock.json index c5429446f..fec23fe9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,10 +89,12 @@ "cypress": "^10.1.0", "delay": "^5.0.0", "dns2": "~2.0.1", + "dompurify": "~2.4.3", "eslint": "~8.14.0", "eslint-plugin-vue": "~8.7.1", "favico.js": "~0.3.10", "jest": "~27.2.5", + "marked": "~4.2.5", "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", "postcss-scss": "~4.0.4", @@ -7801,6 +7803,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "dev": true + }, "node_modules/domutils": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", @@ -13622,6 +13630,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -24880,6 +24900,12 @@ "domelementtype": "^2.3.0" } }, + "dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "dev": true + }, "domutils": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", @@ -29114,6 +29140,12 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true }, + "marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "dev": true + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", diff --git a/package.json b/package.json index 901408363..38d55f4fa 100644 --- a/package.json +++ b/package.json @@ -146,9 +146,11 @@ "cypress": "^10.1.0", "delay": "^5.0.0", "dns2": "~2.0.1", + "dompurify": "~2.4.3", "eslint": "~8.14.0", "eslint-plugin-vue": "~8.7.1", "favico.js": "~0.3.10", + "marked": "~4.2.5", "jest": "~27.2.5", "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", diff --git a/src/assets/app.scss b/src/assets/app.scss index 7da76fff0..f550406fd 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -35,6 +35,11 @@ textarea.form-control { color: $maintenance !important; } +.incident a, +.bg-maintenance a { + color: inherit; +} + .list-group { border-radius: 0.75rem; @@ -248,6 +253,11 @@ optgroup { } } + .incident a, + .bg-maintenance a { + color: inherit; + } + .form-control, .form-control:focus, .form-select, diff --git a/src/lang/en.json b/src/lang/en.json index 231fe29f1..368c64e05 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -25,6 +25,7 @@ "General Monitor Type": "General Monitor Type", "Passive Monitor Type": "Passive Monitor Type", "Specific Monitor Type": "Specific Monitor Type", + "markdownSupported": "Markdown syntax supported", "pauseDashboardHome": "Pause", "Pause": "Pause", "Name": "Name", diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue index f0d87fe5a..00e649381 100644 --- a/src/pages/EditMaintenance.vue +++ b/src/pages/EditMaintenance.vue @@ -21,6 +21,9 @@ +
{{ $t("Powered by") }} {{ $t("Uptime Kuma" ) }} @@ -310,6 +321,8 @@ import ImageCropUpload from "vue-image-crop-upload"; import { PrismEditor } from "vue-prism-editor"; import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere import { useToast } from "vue-toastification"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; import Confirm from "../components/Confirm.vue"; import PublicGroupList from "../components/PublicGroupList.vue"; import MaintenanceTime from "../components/MaintenanceTime.vue"; @@ -477,6 +490,13 @@ export default { return this.overallStatus === STATUS_PAGE_MAINTENANCE; }, + incidentHTML() { + return DOMPurify.sanitize(marked(this.incident.content)); + }, + + footerHTML() { + return DOMPurify.sanitize(marked(this.config.footerText)); + }, }, watch: { @@ -836,6 +856,15 @@ export default { this.config.domainNameList.splice(index, 1); }, + /** + * Generate sanitized HTML from maintenance description + * @param {string} description + * @returns {string} Sanitized HTML + */ + maintenanceHTML(description) { + return DOMPurify.sanitize(marked(description)); + }, + } };