Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _scripts/_undefinedDefaultExport.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default undefined
5 changes: 4 additions & 1 deletion _scripts/webpack.renderer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ const config = {
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',

// Make @fortawesome/vue-fontawesome use the trimmed down API instead of the original @fortawesome/fontawesome-svg-core
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js')
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js'),

// Fix dompurify not being tree-shaking friendly
dompurify$: path.resolve(__dirname, '_undefinedDefaultExport.mjs')
},
extensions: ['.js', '.vue']
},
Expand Down
15 changes: 14 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ export default [
rules: {
'@stylistic/space-before-function-paren': 'off',
'@stylistic/comma-dangle': ['error', 'only-multiline'],
'vue/no-v-html': 'off',

// Ban v-html as it inserts HTML via innerHTML without sanitizing it
// if inserting raw HTML is unavoidable the custom v-safer-html directive should be used
// which sanitizes the HTML before inserting it into the DOM
'vue/no-v-html': 'error',

'no-console': ['error', {
allow: ['warn', 'error'],
Expand Down Expand Up @@ -342,6 +346,15 @@ export default [
}
}
},
{
files: ['src/renderer/directives/vSaferHtml.js'],
languageOptions: {
globals: {
// Fix Sanitizer not being listed in `globals` yet, remove it when it gets added in the future
Sanitizer: 'readable'
}
}
},

...eslintPluginJsonc.configs.base,
{
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@seald-io/nedb": "^4.1.2",
"autolinker": "^4.1.5",
"bgutils-js": "^3.2.0",
"dompurify": "^3.3.3",
"electron-context-menu": "^4.1.1",
"googlevideo": "^4.0.4",
"marked": "^17.0.4",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
</h1>
</template>
<bdo
v-safer-html.lenient="updateChangelog"
class="changeLogText"
dir="ltr"
lang="en"
v-html="updateChangelog"
/>
<FtFlexBox>
<FtButton
Expand Down Expand Up @@ -129,6 +129,7 @@ import FtPlaylistAddVideoPrompt from './components/FtPlaylistAddVideoPrompt/FtPl
import FtCreatePlaylistPrompt from './components/FtCreatePlaylistPrompt/FtCreatePlaylistPrompt.vue'
import FtKeyboardShortcutPrompt from './components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue'
import FtSearchFilters from './components/FtSearchFilters/FtSearchFilters.vue'
import { vSaferHtml } from './directives/vSaferHtml.js'
import store from './store/index'
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/ChannelAbout/ChannelAbout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
>
<h2>{{ $t("Channel.About.Channel Description") }}</h2>
<div
v-safer-html="description"
class="aboutInfo"
dir="auto"
v-html="description"
/>
</template>
<template
Expand Down Expand Up @@ -121,6 +121,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'
import FtChannelBubble from '../../components/FtChannelBubble/FtChannelBubble.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'
import store from '../../store/index'
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/FtCommunityPost/FtCommunityPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@
</p>
</div>
<p
v-safer-html="postText"
class="postText"
dir="auto"
v-html="postText"
/>
<swiper-container
v-if="postType === 'multiImage' && postContent.content.length > 0"
Expand Down Expand Up @@ -177,6 +177,7 @@ import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListPlaylist from '../FtListPlaylist/FtListPlaylist.vue'
import FtCommunityPoll from '../FtCommunityPoll/FtCommunityPoll.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'
import store from '../../store/index'
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/FtListChannel/FtListChannel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
</div>
<p
v-if="listType !== 'grid'"
v-safer-html="description"
class="description"
dir="auto"
v-html="description"
/>
</div>
<FtSubscribeButton
Expand All @@ -80,6 +80,7 @@
import { computed } from 'vue'
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml'
import store from '../../store/index'
Expand Down
25 changes: 20 additions & 5 deletions src/renderer/components/FtTimestampCatcher.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<template>
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
<p
v-safer-html="displayText"
dir="auto"
@timestamp-clicked="catchTimestampClick"
v-html="displayText"
@click="catchTimestampClick"
/>
</template>

<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { vSaferHtml } from '../directives/vSaferHtml.js'
const props = defineProps({
inputHtml: {
type: String,
Expand Down Expand Up @@ -40,15 +43,27 @@ const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):
}).href
// Adding the URL lets the user open the video in a new window at this timestamp
return `<a tabindex="${props.linkTabIndex}" href="${url}" onclick="event.preventDefault();this.dispatchEvent(new CustomEvent('timestamp-clicked',{bubbles:true,detail:${time}}));window.scrollTo(0,0)">${timestamp}</a>`
return `<a tabindex="${props.linkTabIndex}" href="${url}" data-time="${time}">${timestamp}</a>`
}))
const emit = defineEmits(['timestamp-event'])
/**
* @param {CustomEvent} event
* @param {PointerEvent} event
*/
function catchTimestampClick(event) {
emit('timestamp-event', event.detail)
/** @type {HTMLAnchorElement} */
const target = event.target
if (target.tagName === 'A' && target.dataset.time) {
const timeSeconds = parseInt(target.dataset.time)
if (!isNaN(timeSeconds)) {
event.preventDefault()
emit('timestamp-event', timeSeconds)
window.scrollTo(0, 0)
}
}
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@
</p>
</div>
<p
v-safer-html="superChat.message"
class="chatMessage"
dir="auto"
v-html="superChat.message"
/>
</div>
</div>
Expand Down Expand Up @@ -181,9 +181,9 @@
</div>
<p
v-if="comment.message"
v-safer-html="comment.message"
class="chatMessage"
dir="auto"
v-html="comment.message"
/>
</template>
<template
Expand Down Expand Up @@ -219,8 +219,8 @@
>
</span>
<bdi
v-safer-html="comment.message"
class="chatMessage"
v-html="comment.message"
/>
</p>
</template>
Expand Down Expand Up @@ -255,6 +255,7 @@ import { YTNodes } from 'youtubei.js'
import FtLoader from '../FtLoader/FtLoader.vue'
import FtCard from '../ft-card/ft-card.vue'
import FtButton from '../FtButton/FtButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'
import store from '../../store/index'
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import {
} from '../../helpers/utils'
import { deArrowData, deArrowThumbnail } from '../../helpers/sponsorblock'
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

export default defineComponent({
name: 'FtListVideo',
components: {
'ft-icon-button': FtIconButton
},
directives: {
'safer-html': vSaferHtml
},
props: {
data: {
type: Object,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/ft-list-video/ft-list-video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@
</div>
<p
v-if="description && effectiveListTypeIsList && appearance === 'result'"
v-safer-html="description"
class="description"
dir="auto"
v-html="description"
/>
</div>
</div>
Expand Down
54 changes: 54 additions & 0 deletions src/renderer/directives/vSaferHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import DOMPurify from 'dompurify'

const USE_NATIVE_SANITIZER = process.env.IS_ELECTRON || ('Sanitizer' in window && typeof HTMLElement.prototype.setHTML === 'function')

let sanitizer
/** @type {import('dompurify').Config | undefined} */
let domPurifyStrictConfig

/** @type {import('vue').FunctionDirective<HTMLElement, string, 'lenient'>} */
export const vSaferHtml = (element, { value, oldValue, modifiers }) => {
if (oldValue === null || value !== oldValue) {
if (modifiers.lenient) {
// Use the default browser sanitizer configuration e.g. for the changelog
if (USE_NATIVE_SANITIZER) {
element.setHTML(value)
} else {
element.innerHTML = DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false })
}
} else if (USE_NATIVE_SANITIZER) {
// Use a much stricter sanitzer configuration, should be used in most places
if (sanitizer === undefined) {
sanitizer = new Sanitizer({
comments: false,
elements: [
'br',
'b',
'i',
's',
{
name: 'a',
attributes: ['data-time', 'dir', 'href', 'lang', 'tabindex']
},
// live chat emojis (see parseLocalTextRuns)
{
name: 'img',
attributes: ['alt', 'height', 'loading', 'src', 'style', 'width']
}
]
})
}

element.setHTML(value, { sanitizer })
} else {
if (domPurifyStrictConfig === undefined) {
domPurifyStrictConfig = {
ALLOWED_TAGS: ['br', 'b', 'i', 's', 'a', 'img'],
ALLOWED_ATTR: ['alt', 'data-time', 'dir', 'height', 'href', 'lang', 'loading', 'src', 'style', 'tabindex', 'width']
}
}

element.innerHTML = DOMPurify.sanitize(value, domPurifyStrictConfig)
}
}
}
8 changes: 2 additions & 6 deletions src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import store from '../../store/index'
import {
calculatePublishedDate,
getRelativeTimeFromDate,
stripHTML,
} from '../utils'
import { calculatePublishedDate, getRelativeTimeFromDate } from '../utils'
import { isNullOrEmpty } from '../strings'
import autolinker from 'autolinker'
import { FormatUtils, Misc, Player } from 'youtubei.js'
Expand Down Expand Up @@ -616,7 +612,7 @@ function parseInvidiousCommentData(response) {
authorThumb: youtubeImageUrlToInvidious(comment.authorThumbnails.at(-1).url),
author: comment.author,
likes: comment.likeCount,
text: autolinker.link(stripHTML(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstanceUrl()))),
text: autolinker.link(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstanceUrl())),
dataType: 'invidious',
isOwner: comment.authorIsChannelOwner,
isPinned: comment.isPinned,
Expand Down
9 changes: 0 additions & 9 deletions src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,6 @@ export function createWebURL(path) {
return `${origin}${windowPath}/${path}`
}

/**
* strip html tags but keep <br>, <b>, </b> <s>, </s>, <i>, </i>
* @param {string} value
* @returns {string}
*/
export function stripHTML(value) {
return value.replaceAll(/(<(?!br|\/?[abis]|img>)([^>]+)>)/gi, '')
}

/**
* This formats the duration of a video in seconds into a user friendly timestamp.
* It will return strings like LIVE or UPCOMING, without making any changes
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/views/About/About.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
{{ chunk.title }}
</h3>
<div
v-safer-html="chunk.content"
class="content"
v-html="chunk.content"
/>
</figure>
</section>
Expand All @@ -44,6 +44,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtLogoFull from '../../components/FtLogoFull/FtLogoFull.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'
import { ABOUT_BITCOIN_ADDRESS } from '../../../constants'
import packageDetails from '../../../../package.json'
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1984,6 +1984,11 @@
dependencies:
"@types/node" "*"

"@types/trusted-types@^2.0.7":
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/verror@^1.10.3":
version "1.10.5"
resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1"
Expand Down Expand Up @@ -3664,6 +3669,13 @@ domhandler@^5.0.1, domhandler@^5.0.2:
dependencies:
domelementtype "^2.3.0"

dompurify@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"

domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
Expand Down