refactor: ♻️ use linkify and dompurify to show good descriptions
All checks were successful
repod / xml (push) Successful in 19s
repod / php (push) Successful in 1m1s
repod / nodejs (push) Successful in 1m25s
repod / release (push) Has been skipped

This commit is contained in:
Michel Roux 2024-08-08 11:37:48 +02:00
parent e63ff6ef04
commit e190a9eeb6
6 changed files with 54 additions and 21 deletions

10
package-lock.json generated
View File

@ -13,6 +13,8 @@
"@nextcloud/l10n": "^3.1.0", "@nextcloud/l10n": "^3.1.0",
"@nextcloud/router": "^3.0.1", "@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.16.0", "@nextcloud/vue": "^8.16.0",
"dompurify": "^3.1.6",
"linkify-html": "^4.1.3",
"vue": "^2", "vue": "^2",
"vue-material-design-icons": "^5.3.0", "vue-material-design-icons": "^5.3.0",
"vue-router": "^3", "vue-router": "^3",
@ -7876,6 +7878,14 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/linkify-html": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.1.3.tgz",
"integrity": "sha512-Ejb8X/pOxB4IVqG1U37tnF85UW3JtX+eHudH3zlZ2pODz2e/J7zQ/vj+VDWffwhTecJqdRehhluwrRmKoJz+iQ==",
"peerDependencies": {
"linkifyjs": "^4.0.0"
}
},
"node_modules/linkify-it": { "node_modules/linkify-it": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",

View File

@ -19,6 +19,8 @@
"@nextcloud/l10n": "^3.1.0", "@nextcloud/l10n": "^3.1.0",
"@nextcloud/router": "^3.0.1", "@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.16.0", "@nextcloud/vue": "^8.16.0",
"dompurify": "^3.1.6",
"linkify-html": "^4.1.3",
"vue": "^2", "vue": "^2",
"vue-material-design-icons": "^5.3.0", "vue-material-design-icons": "^5.3.0",
"vue-router": "^3", "vue-router": "^3",

View File

@ -1,9 +1,8 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div> <div>
<NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" /> <NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" />
<h2>{{ name }}</h2> <h2>{{ name }}</h2>
<p v-html="strippedDescription" /> <SafeHtml :source="description" />
<div> <div>
<NcButton v-if="link" :href="link" target="_blank"> <NcButton v-if="link" :href="link" target="_blank">
<template #icon> <template #icon>
@ -25,7 +24,7 @@
import { NcAvatar, NcButton } from '@nextcloud/vue' import { NcAvatar, NcButton } from '@nextcloud/vue'
import DownloadIcon from 'vue-material-design-icons/Download.vue' import DownloadIcon from 'vue-material-design-icons/Download.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue' import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import { cleanHtml } from '../../utils/text.js' import SafeHtml from './SafeHtml.vue'
import { humanFileSize } from '../../utils/size.js' import { humanFileSize } from '../../utils/size.js'
export default { export default {
@ -35,6 +34,7 @@ export default {
NcAvatar, NcAvatar,
NcButton, NcButton,
OpenInNewIcon, OpenInNewIcon,
SafeHtml,
}, },
props: { props: {
description: { description: {
@ -70,9 +70,6 @@ export default {
episodeFileSize() { episodeFileSize() {
return humanFileSize(this.size) return humanFileSize(this.size)
}, },
strippedDescription() {
return cleanHtml(this.description)
},
}, },
} }
</script> </script>

View File

@ -0,0 +1,36 @@
<template>
<div v-sanitize="source" class="html" />
</template>
<script>
import linkifyHtml from 'linkify-html'
import { sanitize } from 'dompurify'
export default {
name: 'SafeHtml',
directives: {
sanitize: {
inserted(el, binding) {
el.innerHTML = sanitize(
linkifyHtml(binding.value, {
nl2br: true,
target: '_blank',
}),
)
},
},
},
props: {
source: {
type: String,
required: true,
},
},
}
</script>
<style>
.html a {
text-decoration: underline;
}
</style>

View File

@ -1,4 +1,3 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div class="header"> <div class="header">
<img class="background" :src="imageUrl" /> <img class="background" :src="imageUrl" />
@ -21,9 +20,7 @@
<i>{{ author }}</i> <i>{{ author }}</i>
</a> </a>
<br /><br /> <br /><br />
<p> <SafeHtml :source="description" />
<small v-html="strippedDescription" />
</p>
</div> </div>
<NcAppNavigationNew <NcAppNavigationNew
v-if="!isSubscribed" v-if="!isSubscribed"
@ -43,8 +40,8 @@ import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue'
import { showError, showSuccess } from '@nextcloud/dialogs' import { showError, showSuccess } from '@nextcloud/dialogs'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import RssIcon from 'vue-material-design-icons/Rss.vue' import RssIcon from 'vue-material-design-icons/Rss.vue'
import SafeHtml from '../Atoms/SafeHtml.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { cleanHtml } from '../../utils/text.js'
import { decodeUrl } from '../../utils/url.js' import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
@ -55,6 +52,7 @@ export default {
NcAppNavigationNew, NcAppNavigationNew,
PlusIcon, PlusIcon,
RssIcon, RssIcon,
SafeHtml,
}, },
props: { props: {
author: { author: {
@ -85,9 +83,6 @@ export default {
isSubscribed() { isSubscribed() {
return this.$store.state.subscriptions.subscriptions.includes(this.url) return this.$store.state.subscriptions.subscriptions.includes(this.url)
}, },
strippedDescription() {
return cleanHtml(this.description)
},
}, },
methods: { methods: {
async addSubscription() { async addSubscription() {

View File

@ -1,7 +0,0 @@
// https://stackoverflow.com/a/5002618
export const cleanHtml = (text) => {
const pre = document.createElement('pre')
pre.innerHTML = text.replace(/<br\s*\/?>/gm, '\n')
const strippedText = pre.textContent || pre.innerText || ''
return strippedText.replace(/\n/gm, '<br>')
}