Replace sanitize-html by dompurify in frontend
It's lighter and we don't have native dependencies warnings in web browser
This commit is contained in:
parent
38cc3910ff
commit
16d9204ea8
|
@ -69,6 +69,7 @@
|
|||
"@types/chart.js": "^2.9.37",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jschannel": "^1.0.0",
|
||||
"@types/linkifyjs": "^2.1.2",
|
||||
"@types/lodash-es": "^4.17.0",
|
||||
|
@ -93,6 +94,7 @@
|
|||
"chartjs-plugin-zoom": "~2.0.1",
|
||||
"core-js": "^3.22.8",
|
||||
"debug": "^4.3.1",
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.1.0",
|
||||
|
@ -109,7 +111,6 @@
|
|||
"ngx-uploadx": "^6.1.0",
|
||||
"primeng": "^17.3.1",
|
||||
"rxjs": "^7.3.0",
|
||||
"sanitize-html": "^2.1.2",
|
||||
"sha.js": "^2.4.11",
|
||||
"socket.io-client": "^4.5.4",
|
||||
"stylelint": "^16.2.1",
|
||||
|
|
|
@ -1,41 +1,93 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { getCustomMarkupSanitizeOptions, getDefaultSanitizeOptions } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
getDefaultSanitizedHrefAttributes,
|
||||
getDefaultSanitizedSchemes,
|
||||
getDefaultSanitizedTags
|
||||
} from '@peertube/peertube-core-utils'
|
||||
import DOMPurify, { DOMPurifyI } from 'dompurify'
|
||||
import { LinkifierService } from './linkifier.service'
|
||||
|
||||
@Injectable()
|
||||
export class HtmlRendererService {
|
||||
private sanitizeHtml: typeof import ('sanitize-html')
|
||||
private simpleDomPurify: DOMPurifyI
|
||||
private enhancedDomPurify: DOMPurifyI
|
||||
|
||||
constructor (private linkifier: LinkifierService) {
|
||||
this.simpleDomPurify = DOMPurify()
|
||||
this.enhancedDomPurify = DOMPurify()
|
||||
|
||||
this.addHrefHook(this.simpleDomPurify)
|
||||
this.addHrefHook(this.enhancedDomPurify)
|
||||
|
||||
this.addCheckSchemesHook(this.simpleDomPurify, getDefaultSanitizedSchemes())
|
||||
this.addCheckSchemesHook(this.simpleDomPurify, [ ...getDefaultSanitizedSchemes(), 'mailto' ])
|
||||
}
|
||||
|
||||
async convertToBr (text: string) {
|
||||
await this.loadSanitizeHtml()
|
||||
private addHrefHook (dompurifyInstance: DOMPurifyI) {
|
||||
dompurifyInstance.addHook('afterSanitizeAttributes', node => {
|
||||
if ('target' in node) {
|
||||
node.setAttribute('target', '_blank')
|
||||
|
||||
const html = text.replace(/\r?\n/g, '<br />')
|
||||
const rel = node.hasAttribute('rel')
|
||||
? node.getAttribute('rel') + ' '
|
||||
: ''
|
||||
|
||||
return this.sanitizeHtml(html, {
|
||||
allowedTags: [ 'br' ]
|
||||
node.setAttribute('rel', rel + 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
|
||||
const [ html ] = await Promise.all([
|
||||
// Convert possible markdown to html
|
||||
this.linkifier.linkify(text),
|
||||
private addCheckSchemesHook (dompurifyInstance: DOMPurifyI, schemes: string[]) {
|
||||
const regex = new RegExp(`^(${schemes.join('|')}):`, 'im')
|
||||
|
||||
this.loadSanitizeHtml()
|
||||
])
|
||||
dompurifyInstance.addHook('afterSanitizeAttributes', node => {
|
||||
const anchor = document.createElement('a')
|
||||
|
||||
const options = additionalAllowedTags.length !== 0
|
||||
? getCustomMarkupSanitizeOptions(additionalAllowedTags)
|
||||
: getDefaultSanitizeOptions()
|
||||
if (node.hasAttribute('href')) {
|
||||
anchor.href = node.getAttribute('href')
|
||||
|
||||
return this.sanitizeHtml(html, options)
|
||||
if (anchor.protocol && !anchor.protocol.match(regex)) {
|
||||
node.removeAttribute('href')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async loadSanitizeHtml () {
|
||||
this.sanitizeHtml = (await import('sanitize-html')).default
|
||||
convertToBr (text: string) {
|
||||
const html = text.replace(/\r?\n/g, '<br />')
|
||||
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [ 'br' ]
|
||||
})
|
||||
}
|
||||
|
||||
async toSimpleSafeHtml (text: string) {
|
||||
const html = await this.linkifier.linkify(text)
|
||||
|
||||
return this.sanitize(this.simpleDomPurify, html)
|
||||
}
|
||||
|
||||
async toCustomPageSafeHtml (text: string, additionalAllowedTags: string[] = []) {
|
||||
const html = await this.linkifier.linkify(text)
|
||||
|
||||
const enhancedTags = [ 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ]
|
||||
|
||||
return this.sanitize(this.enhancedDomPurify, html, {
|
||||
additionalTags: [ ...enhancedTags, ...additionalAllowedTags ],
|
||||
additionalAttributes: [ 'src', 'alt', 'style' ]
|
||||
})
|
||||
}
|
||||
|
||||
private sanitize (domPurify: DOMPurifyI, html: string, options: {
|
||||
additionalTags?: string[]
|
||||
additionalAttributes?: string[]
|
||||
} = {}) {
|
||||
const { additionalTags = [], additionalAttributes = [] } = options
|
||||
|
||||
return domPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [ ...getDefaultSanitizedTags(), ...additionalTags ],
|
||||
ALLOWED_ATTR: [ ...getDefaultSanitizedHrefAttributes(), ...additionalAttributes ],
|
||||
ALLOW_DATA_ATTR: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import MarkdownIt from 'markdown-it'
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
buildVideoLink,
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
TEXT_RULES,
|
||||
TEXT_WITH_HTML_RULES
|
||||
} from '@peertube/peertube-core-utils'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { HtmlRendererService } from './html-renderer.service'
|
||||
|
||||
type MarkdownParsers = {
|
||||
|
@ -140,7 +140,13 @@ export class MarkdownService {
|
|||
|
||||
const html = this.markdownParsers[name].render(markdown)
|
||||
|
||||
if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
|
||||
if (config.escape) {
|
||||
if (name === 'customPageMarkdownIt') {
|
||||
return this.htmlRenderer.toCustomPageSafeHtml(html, additionalAllowedTags)
|
||||
}
|
||||
|
||||
return this.htmlRenderer.toSimpleSafeHtml(html)
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ export class ConfirmComponent implements OnInit {
|
|||
|
||||
this.confirmButtonText = confirmButtonText || $localize`Confirm`
|
||||
|
||||
this.html.toSafeHtml(message)
|
||||
this.html.toSimpleSafeHtml(message)
|
||||
.then(html => {
|
||||
this.message = html
|
||||
|
||||
|
|
|
@ -113,12 +113,12 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
|||
private loadMessages () {
|
||||
this.abuseService.listAbuseMessages(this.abuse)
|
||||
.subscribe({
|
||||
next: async res => {
|
||||
next: res => {
|
||||
this.abuseMessages = []
|
||||
|
||||
for (const m of res.data) {
|
||||
this.abuseMessages.push(Object.assign(m, {
|
||||
messageHtml: await this.htmlRenderer.convertToBr(m.message)
|
||||
messageHtml: this.htmlRenderer.convertToBr(m.message)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -2806,6 +2806,13 @@
|
|||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/dompurify@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
|
||||
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
|
||||
|
@ -3071,6 +3078,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
||||
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
||||
|
||||
"@types/trusted-types@*":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@types/video.js@^7.3.40":
|
||||
version "7.3.58"
|
||||
resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.58.tgz#7e8cdafee25c75d6eb18f530b93ac52edff53c03"
|
||||
|
@ -5166,11 +5178,6 @@ deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0:
|
|||
resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a"
|
||||
integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
default-browser-id@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26"
|
||||
|
@ -5386,6 +5393,11 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
|||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
|
||||
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||
|
@ -9248,11 +9260,6 @@ parse-node-version@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
|
||||
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
|
||||
|
||||
parse-srcset@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
||||
|
||||
parse5-html-rewriting-stream@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36"
|
||||
|
@ -9498,7 +9505,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
|||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@8.4.38, postcss@^8.2.14, postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38:
|
||||
postcss@8.4.38, postcss@^8.2.14, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
|
@ -10164,18 +10171,6 @@ safe-stable-stringify@^2.3.1:
|
|||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sanitize-html@^2.1.2:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae"
|
||||
integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
htmlparser2 "^8.0.0"
|
||||
is-plain-object "^5.0.0"
|
||||
parse-srcset "^1.0.2"
|
||||
postcss "^8.3.11"
|
||||
|
||||
sass-loader@14.2.1:
|
||||
version "14.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-14.2.1.tgz#db9ad96b56dc1c1ea546101e76375d5b008fec70"
|
||||
|
|
|
@ -1,11 +1,30 @@
|
|||
export function getDefaultSanitizedTags () {
|
||||
return [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ]
|
||||
}
|
||||
|
||||
export function getDefaultSanitizedSchemes () {
|
||||
return [ 'http', 'https' ]
|
||||
}
|
||||
|
||||
export function getDefaultSanitizedHrefAttributes () {
|
||||
return [ 'href', 'class', 'target', 'rel' ]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sanitize-html
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getDefaultSanitizeOptions () {
|
||||
return {
|
||||
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
|
||||
allowedSchemes: [ 'http', 'https' ],
|
||||
allowedTags: getDefaultSanitizedTags(),
|
||||
allowedSchemes: getDefaultSanitizedSchemes(),
|
||||
allowedAttributes: {
|
||||
'a': [ 'href', 'class', 'target', 'rel' ],
|
||||
'a': getDefaultSanitizedHrefAttributes(),
|
||||
'*': [ 'data-*' ]
|
||||
},
|
||||
|
||||
transformTags: {
|
||||
a: (tagName: string, attribs: any) => {
|
||||
let rel = 'noopener noreferrer'
|
||||
|
@ -29,28 +48,9 @@ export function getTextOnlySanitizeOptions () {
|
|||
}
|
||||
}
|
||||
|
||||
export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
|
||||
const base = getDefaultSanitizeOptions()
|
||||
|
||||
return {
|
||||
allowedTags: [
|
||||
...base.allowedTags,
|
||||
...additionalAllowedTags,
|
||||
'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img'
|
||||
],
|
||||
allowedSchemes: [
|
||||
...base.allowedSchemes,
|
||||
|
||||
'mailto'
|
||||
],
|
||||
allowedAttributes: {
|
||||
...base.allowedAttributes,
|
||||
|
||||
'img': [ 'src', 'alt' ],
|
||||
'*': [ 'data-*', 'style' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Manual escapes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Thanks: https://stackoverflow.com/a/12034334
|
||||
export function escapeHTML (stringParam: string) {
|
||||
|
|
Loading…
Reference in New Issue